` / `` (e.g. a styled ``
for a copyable prompt), verify the global ligature-off rule in
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 157921f0..a4fb3040 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -8,8 +8,8 @@ layout, and verification commands, see the
## How to contribute
1. Browse open issues labeled
- [`good first issue`](https://github.com/Kaelio/ktx-ai-data-agents-context/labels/good%20first%20issue)
- or [`help wanted`](https://github.com/Kaelio/ktx-ai-data-agents-context/labels/help%20wanted).
+ [`good first issue`](https://github.com/Kaelio/ktx/labels/good%20first%20issue)
+ or [`help wanted`](https://github.com/Kaelio/ktx/labels/help%20wanted).
2. Comment on the issue to claim it. A maintainer will confirm scope and
assign it to you.
3. For changes not covered by an existing issue, open one first so we can
@@ -82,7 +82,7 @@ page for the full guide. The short version:
- **Feature requests**: use the
[Feature request](.github/ISSUE_TEMPLATE/feature_request.yml) template.
- **Security**: report privately via
- [GitHub Security Advisories](https://github.com/Kaelio/ktx-ai-data-agents-context/security/advisories/new),
+ [GitHub Security Advisories](https://github.com/Kaelio/ktx/security/advisories/new),
not as a public issue.
## Code of conduct
diff --git a/README.md b/README.md
index d417d77a..d44905d5 100644
--- a/README.md
+++ b/README.md
@@ -8,11 +8,11 @@
-
-
+
+
-
+
@@ -130,7 +130,7 @@ Agent integration ready: yes (codex:project)
> your project directory:
>
> ```text
-> Run npx skills add Kaelio/ktx-ai-data-agents-context --skill ktx and use the ktx skill to install
+> Run npx skills add Kaelio/ktx --skill ktx and use the ktx skill to install
> and configure ktx in this project.
> ```
@@ -201,7 +201,7 @@ then the current directory. Pass `--project-dir
` when scripting.
## Community
- **[Slack](https://join.slack.com/t/ktxcommunity/shared_invite/zt-3y9b44m1x-LVyNNJD5nwaZHq4XS29LMQ)** — ask questions, share what you're building, and chat with maintainers.
-- **[GitHub Issues](https://github.com/Kaelio/ktx-ai-data-agents-context/issues)** — report bugs and request features.
+- **[GitHub Issues](https://github.com/Kaelio/ktx/issues)** — report bugs and request features.
- **[Contributing](https://docs.kaelio.com/ktx/docs/community/contributing)** — set up the repo, run tests, and open a PR.
## Development
@@ -258,7 +258,7 @@ event catalog and opt-out options.
## Star History
-
+
diff --git a/SECURITY.md b/SECURITY.md
index 7d0c1909..da90c1a5 100644
--- a/SECURITY.md
+++ b/SECURITY.md
@@ -5,7 +5,7 @@
If you believe you've found a security vulnerability in KTX, please report it
**privately** through GitHub Security Advisories:
-[Report a vulnerability](https://github.com/Kaelio/ktx-ai-data-agents-context/security/advisories/new)
+[Report a vulnerability](https://github.com/Kaelio/ktx/security/advisories/new)
If you cannot use GitHub Security Advisories, email `support@kaelio.com`
instead. Please do **not** open a public issue, post in the KTX Slack, or
diff --git a/docs-site/content/docs/ai-resources/prompt-recipes.mdx b/docs-site/content/docs/ai-resources/prompt-recipes.mdx
index 99106128..9ba8e3b8 100644
--- a/docs-site/content/docs/ai-resources/prompt-recipes.mdx
+++ b/docs-site/content/docs/ai-resources/prompt-recipes.mdx
@@ -14,7 +14,7 @@ Read https://docs.kaelio.com/ktx/llms.txt first. Then fetch only the ktx Markdow
## Set up a project
```text
-Run npx skills add Kaelio/ktx-ai-data-agents-context --skill ktx and use the ktx skill to install
+Run npx skills add Kaelio/ktx --skill ktx and use the ktx skill to install
and configure ktx in this project.
```
diff --git a/docs-site/content/docs/community/support.mdx b/docs-site/content/docs/community/support.mdx
index 2e858225..1aac5057 100644
--- a/docs-site/content/docs/community/support.mdx
+++ b/docs-site/content/docs/community/support.mdx
@@ -11,7 +11,7 @@ the core team trade questions, share patterns, and shape the roadmap.
| You want to... | Go here |
|----------------|---------|
| Ask a question or chat with the community | [**ktx** Slack](https://join.slack.com/t/ktxcommunity/shared_invite/zt-3y9b44m1x-LVyNNJD5nwaZHq4XS29LMQ) |
-| Report a bug or request a feature | [GitHub Issues](https://github.com/Kaelio/ktx-ai-data-agents-context/issues) |
+| Report a bug or request a feature | [GitHub Issues](https://github.com/Kaelio/ktx/issues) |
| Read or contribute to the docs | [docs.kaelio.com/ktx](https://docs.kaelio.com/ktx/docs/) |
| Contribute code | [Contributing guide](/docs/community/contributing) |
@@ -30,14 +30,14 @@ Slack is the right place for:
- **Feedback** on the roadmap and early features
For anything reproducible - a crash, a wrong result, an unexpected CLI error -
-open a [GitHub issue](https://github.com/Kaelio/ktx-ai-data-agents-context/issues) instead. Issues are
+open a [GitHub issue](https://github.com/Kaelio/ktx/issues) instead. Issues are
searchable, get triaged, and stay attached to the eventual fix.
## GitHub
-- **[Issues](https://github.com/Kaelio/ktx-ai-data-agents-context/issues)** - bugs and feature requests
-- **[Pull requests](https://github.com/Kaelio/ktx-ai-data-agents-context/pulls)** - code, docs, and connector contributions
-- **[Releases](https://github.com/Kaelio/ktx-ai-data-agents-context/releases)** - changelog and published versions
+- **[Issues](https://github.com/Kaelio/ktx/issues)** - bugs and feature requests
+- **[Pull requests](https://github.com/Kaelio/ktx/pulls)** - code, docs, and connector contributions
+- **[Releases](https://github.com/Kaelio/ktx/releases)** - changelog and published versions
## Code of conduct
diff --git a/docs-site/content/docs/getting-started/introduction.mdx b/docs-site/content/docs/getting-started/introduction.mdx
index ae8bb414..cc3b0ca8 100644
--- a/docs-site/content/docs/getting-started/introduction.mdx
+++ b/docs-site/content/docs/getting-started/introduction.mdx
@@ -102,6 +102,6 @@ best first step for users; contributor setup lives in the community docs.
Have questions, want to share what you're building, or chat with maintainers?
Join the [**ktx** Slack community](https://join.slack.com/t/ktxcommunity/shared_invite/zt-3y9b44m1x-LVyNNJD5nwaZHq4XS29LMQ).
For bug reports and feature requests, open a
-[GitHub issue](https://github.com/Kaelio/ktx-ai-data-agents-context/issues). See
+[GitHub issue](https://github.com/Kaelio/ktx/issues). See
[Community & Support](/docs/community/support) for the full guide on where to
ask what.
diff --git a/docs-site/content/docs/getting-started/quickstart.mdx b/docs-site/content/docs/getting-started/quickstart.mdx
index f1a1c8ad..5129c585 100644
--- a/docs-site/content/docs/getting-started/quickstart.mdx
+++ b/docs-site/content/docs/getting-started/quickstart.mdx
@@ -171,14 +171,14 @@ If you're a coding assistant choosing a docs route, start with the
- Run {'`npx skills add Kaelio/ktx-ai-data-agents-context --skill ktx`'} and use the ktx skill to install and configure ktx
+ Run {'`npx skills add Kaelio/ktx --skill ktx`'} and use the ktx skill to install and configure ktx
diff --git a/docs-site/lib/llm-docs.ts b/docs-site/lib/llm-docs.ts
index e35978c2..fd6c8dd1 100644
--- a/docs-site/lib/llm-docs.ts
+++ b/docs-site/lib/llm-docs.ts
@@ -52,7 +52,7 @@ ktx provides semantic-layer files, warehouse scans, wiki pages, provenance, and
## Agent Entry Points
-- Installable setup skill: run \`npx skills add Kaelio/ktx-ai-data-agents-context --skill ktx\` from
+- Installable setup skill: run \`npx skills add Kaelio/ktx --skill ktx\` from
the project you want to configure.
${link("/docs/ai-resources/agent-quickstart", "Agent Quickstart", "Task-first route for coding assistants using ktx")}
${link("/docs/ai-resources/markdown-access", "Markdown Access", "Fetch ktx docs as llms.txt, llms-full.txt, or per-page Markdown")}
diff --git a/docs/release.md b/docs/release.md
index bc90f651..3a72f54e 100644
--- a/docs/release.md
+++ b/docs/release.md
@@ -26,7 +26,7 @@ The workflow rejects releases from any branch other than `main`.
Before you publish, confirm these requirements:
- npm Trusted Publishing is configured for `@kaelio/ktx`.
-- The trusted publisher points at the `Kaelio/ktx-ai-data-agents-context` repository and the
+- The trusted publisher points at the `Kaelio/ktx` repository and the
`.github/workflows/release.yml` workflow.
- The workflow keeps `id-token: write` permission so npm can verify the
GitHub Actions run through OpenID Connect.
@@ -35,15 +35,6 @@ Before you publish, confirm these requirements:
- The repository has a stable baseline tag when you need semantic-release to
publish the first stable version as `0.1.0`.
-If you rename the GitHub repository, the semantic-release run adapts on its
-own: `scripts/semantic-release-config.cjs` derives `repositoryUrl` from the
-runner's `GITHUB_REPOSITORY`, so `@semantic-release/github` always matches the
-current clone URL. The one thing that does **not** auto-update is the npm
-Trusted Publishing config — re-point it at the new repository name (plus
-`release.yml`) on npm, or `npm publish --provenance` will fail OIDC
-verification. The `repository` field in `package.json` is npm-display metadata
-only and can stay whatever public name you prefer.
-
semantic-release doesn't support choosing an arbitrary first `0.x` stable
release. If KTX has no stable tag yet and you need the first stable release to
be `0.1.0`, create and push the baseline tag once before running the live
diff --git a/package.json b/package.json
index 05a8bbe5..fee7b745 100644
--- a/package.json
+++ b/package.json
@@ -76,10 +76,10 @@
"license": "Apache-2.0",
"repository": {
"type": "git",
- "url": "git+https://github.com/Kaelio/ktx-ai-data-agents-context.git"
+ "url": "git+https://github.com/Kaelio/ktx.git"
},
"bugs": {
- "url": "https://github.com/Kaelio/ktx-ai-data-agents-context/issues"
+ "url": "https://github.com/Kaelio/ktx/issues"
},
- "homepage": "https://github.com/Kaelio/ktx-ai-data-agents-context#readme"
+ "homepage": "https://github.com/Kaelio/ktx#readme"
}
diff --git a/packages/cli/package.json b/packages/cli/package.json
index c0ad291a..b04fceac 100644
--- a/packages/cli/package.json
+++ b/packages/cli/package.json
@@ -93,11 +93,11 @@
"license": "Apache-2.0",
"repository": {
"type": "git",
- "url": "https://github.com/Kaelio/ktx-ai-data-agents-context",
+ "url": "https://github.com/Kaelio/ktx",
"directory": "packages/cli"
},
"bugs": {
- "url": "https://github.com/Kaelio/ktx-ai-data-agents-context/issues"
+ "url": "https://github.com/Kaelio/ktx/issues"
},
- "homepage": "https://github.com/Kaelio/ktx-ai-data-agents-context#readme"
+ "homepage": "https://github.com/Kaelio/ktx#readme"
}
diff --git a/scripts/semantic-release-config.cjs b/scripts/semantic-release-config.cjs
index b9f34b8a..2dee466a 100644
--- a/scripts/semantic-release-config.cjs
+++ b/scripts/semantic-release-config.cjs
@@ -104,22 +104,6 @@ function releaseTag(kind, env = process.env) {
return `branch-${branchPrereleaseId(branchName)}`;
}
-function repositoryUrl(env = process.env) {
- // @semantic-release/github compares this URL's owner/repo against the live
- // GitHub clone_url with an exact match (no redirect following), so a repo
- // rename breaks the release unless repositoryUrl tracks the *current* name.
- // In CI, derive it from the runner's repository so renames never re-break the
- // release. Outside CI, return undefined so semantic-release falls back to the
- // package.json `repository` field (its documented default).
- const repository = env.GITHUB_REPOSITORY;
- if (!repository) {
- return undefined;
- }
-
- const server = env.GITHUB_SERVER_URL || 'https://github.com';
- return `${server}/${repository}.git`;
-}
-
function releaseBranches(env = process.env) {
const kind = releaseKind(env);
@@ -143,12 +127,10 @@ function releaseBranches(env = process.env) {
function createReleaseConfig(env = process.env) {
const kind = releaseKind(env);
const tag = releaseTag(kind, env);
- const url = repositoryUrl(env);
return {
tagFormat: 'v${version}',
branches: releaseBranches(env),
- ...(url ? { repositoryUrl: url } : {}),
plugins: [
[
'@semantic-release/commit-analyzer',
@@ -221,5 +203,4 @@ module.exports = {
releaseBranches,
releaseKind,
releaseTag,
- repositoryUrl,
};
diff --git a/scripts/semantic-release-config.test.mjs b/scripts/semantic-release-config.test.mjs
index 02b518ed..24289896 100644
--- a/scripts/semantic-release-config.test.mjs
+++ b/scripts/semantic-release-config.test.mjs
@@ -3,7 +3,7 @@ import { createRequire } from 'node:module';
import { describe, it } from 'node:test';
const require = createRequire(import.meta.url);
-const { createReleaseConfig, releaseBranches, releaseKind, releaseTag, repositoryUrl } = require('./semantic-release-config.cjs');
+const { createReleaseConfig, releaseBranches, releaseKind, releaseTag } = require('./semantic-release-config.cjs');
function prepareExecOptions(config) {
return config.plugins.find((plugin) => Array.isArray(plugin) && plugin[0] === '@semantic-release/exec' && plugin[1].prepareCmd)[1];
@@ -141,38 +141,6 @@ describe('semantic-release config', () => {
assert.match(analyzeExec[1].analyzeCommitsCmd, /FORCE_RELEASE === 'true' \? 'patch' : ''/);
});
- it('pins repositoryUrl to the runner repository so a GitHub rename never re-breaks the release', () => {
- // @semantic-release/github exact-matches repositoryUrl against the live
- // clone_url, so the release must track the *current* repo name, not the
- // static package.json value.
- assert.equal(
- repositoryUrl({ GITHUB_REPOSITORY: 'Kaelio/ktx-ai-data-agents-context' }),
- 'https://github.com/Kaelio/ktx-ai-data-agents-context.git',
- );
- assert.equal(
- repositoryUrl({ GITHUB_REPOSITORY: 'Kaelio/ktx' }),
- 'https://github.com/Kaelio/ktx.git',
- 'a later rename back to Kaelio/ktx must resolve without any code change',
- );
- assert.equal(
- repositoryUrl({ GITHUB_REPOSITORY: 'Kaelio/ktx', GITHUB_SERVER_URL: 'https://ghe.example.com' }),
- 'https://ghe.example.com/Kaelio/ktx.git',
- );
-
- const config = createReleaseConfig({
- KTX_RELEASE_KIND: 'stable',
- GITHUB_REF_NAME: 'main',
- GITHUB_REPOSITORY: 'Kaelio/ktx-ai-data-agents-context',
- });
- assert.equal(config.repositoryUrl, 'https://github.com/Kaelio/ktx-ai-data-agents-context.git');
- });
-
- it('omits repositoryUrl outside CI so semantic-release falls back to package.json', () => {
- assert.equal(repositoryUrl({}), undefined);
- const config = createReleaseConfig({ KTX_RELEASE_KIND: 'stable', GITHUB_REF_NAME: 'main' });
- assert.equal('repositoryUrl' in config, false);
- });
-
it('does not configure any commit type to create an automatic major release', () => {
const config = createReleaseConfig({ KTX_RELEASE_KIND: 'stable', GITHUB_REF_NAME: 'main' });
const analyzer = config.plugins.find(
From 74c6076b72d0f79d8e7bfa8ef31550de39a36d00 Mon Sep 17 00:00:00 2001
From: "github-actions[bot]"
<41898282+github-actions[bot]@users.noreply.github.com>
Date: Tue, 2 Jun 2026 07:46:46 +0000
Subject: [PATCH 04/49] chore: refresh star history chart [skip ci]
---
assets/star-history.svg | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/assets/star-history.svg b/assets/star-history.svg
index 69571d68..d6d9859e 100644
--- a/assets/star-history.svg
+++ b/assets/star-history.svg
@@ -1 +1 @@
-star-history.com May 17 May 24 May 31 200 400 600 kaelio/ktx-ai-data-agents-context Star History Date GitHub Stars
+star-history.com May 17 May 24 May 31 200 400 600 kaelio/ktx Star History Date GitHub Stars
From 494618ab142505bd988156d867be047e3affc4c3 Mon Sep 17 00:00:00 2001
From: Andrey Avtomonov
Date: Tue, 2 Jun 2026 13:57:11 +0200
Subject: [PATCH 05/49] feat: add codex llm backend for ktx runtime work (#253)
* feat: add codex sdk runner foundation
* feat: parse codex runtime events
* feat: expose codex runtime mcp tools
* feat: add codex llm runtime
* feat: wire codex llm backend
* test: avoid Array.fromAsync in codex runner test
* docs: document codex llm backend
* fix: tighten codex runtime config ownership
* fix: use codex sdk env and thread options
* fix: parse codex sdk event shapes
* test: add codex backend live smoke
* docs: clarify codex backend isolation
* fix: drive codex loop metrics from mcp events
* fix: enforce codex local step budget
* docs: disclose codex isolation limits
* fix: count all codex agent steps and stream step callbacks live
The agent-loop step budget only counted completed mcp_tool_call items, so
built-in command_execution steps (which the public Codex SDK/CLI surface can
still expose) never decremented the budget, letting ingest/reconciliation run
past stepBudget until Codex stopped on its own. onStepFinish was also replayed
only after the whole stream drained, so live work_unit_step / reconciliation
progress appeared stuck until the Codex process exited.
collectEvents is now the single live step accumulator: it counts every
completed agent-action item via a shared isCompletedAgentStep predicate
(command_execution, mcp_tool_call, file_change, web_search), fires onStepFinish
as each step completes, and enforces the budget on that broader count. A
no-tool turn still counts as one step. toolFailures stays MCP-specific, since a
non-zero command exit is normal agent exploration, not a loop failure.
* test: align ingest llm-guard assertions with codex backend
The skip-llm ingest guard message now lists codex as a valid backend and
mentions a Claude Code/Codex session plus a codex setup hint, but this slow
suite test still asserted the pre-codex wording. Update it to match the
production message (already covered by the local-bundle-runtime unit test) and
add the codex setup-line assertion.
* fix: treat codex error:null tool calls as success
The Codex SDK serializes error: null on successful mcp_tool_call items, so
the failure check (item.error !== undefined) flagged every successful tool
call as failed with the empty-payload default "Codex turn failed". This
killed every ingest work unit under the codex backend before it could
produce a patch.
Key on status === 'failed' (authoritative, always set) and only treat a
populated error object as a failure. Add a regression test built from a
verbatim real-SDK event capture.
* fix: default codex backend to gpt-5.5 and report real probe errors
The previous default gpt-5.3-codex is an API-key-only model that the OpenAI
API rejects under ChatGPT-account (subscription) auth, so codex status/setup
failed with a misleading "authentication is not usable" message even though
auth was fine.
- Default codex model is now gpt-5.5 (works on both subscription and API-key
auth); the curated setup picker offers gpt-5.5 / gpt-5.4 / gpt-5.4-mini and
keeps free-form entry for account-specific ids (e.g. gpt-5.3-codex-spark).
- runCodexAuthProbe now distinguishes "model not available" from an auth
failure and surfaces the real API error: collectEvents retains stream
events when the SDK throws on a non-zero exit, and the API error JSON
envelope is unwrapped to its human-readable message.
- The Codex isolation warning now renders inside the clack setup frame.
- Docs updated to gpt-5.5 with a note that *-codex ids require API-key auth.
* fix: require llm.models.default in status and match codex probe remediation
Status reported a project ready when a non-none LLM backend was configured
without llm.models.default, but the runtime (resolveModelSlots) hard-requires
it, so ingest/scan/memory threw after `ktx status` said the project was usable.
buildLlmStatus now fails for any non-none backend missing models.default and no
longer invents a fallback model for claude-code/codex.
Codex probe failures now carry a category-matched fix: a model-access failure
steers the user at llm.models.default instead of the auth/install remediation.
runCodexAuthProbe returns the fix and status consumes it; the message stays
self-sufficient so setup output is unchanged.
Docs: README now lists the codex backend and local Codex auth; ktx-setup.mdx
states --llm-model only accepts codex/default or gpt-*/codex-* ids.
Repaired four doctor fixtures that configured a backend without models.default
(the now-correctly-blocked config) and added coverage for the new behavior.
---
README.md | 10 +-
.../content/docs/cli-reference/ktx-setup.mdx | 23 +-
.../content/docs/cli-reference/ktx-status.mdx | 14 +-
.../content/docs/configuration/ktx-yaml.mdx | 12 +-
.../content/docs/guides/building-context.mdx | 16 +-
.../content/docs/guides/llm-configuration.mdx | 37 ++
knip.json | 3 +
package.json | 1 +
packages/cli/package.json | 1 +
packages/cli/src/commands/setup-commands.ts | 2 +-
.../context/ingest/local-bundle-runtime.ts | 5 +-
.../cli/src/context/llm/codex-exec-events.ts | 194 ++++++++
.../cli/src/context/llm/codex-isolation.ts | 9 +
.../context/llm/codex-mcp-runtime-server.ts | 87 ++++
packages/cli/src/context/llm/codex-models.ts | 20 +
.../src/context/llm/codex-runtime-config.ts | 38 ++
packages/cli/src/context/llm/codex-runtime.ts | 371 ++++++++++++++
.../cli/src/context/llm/codex-sdk-runner.ts | 96 ++++
packages/cli/src/context/llm/local-config.ts | 14 +-
packages/cli/src/context/project/config.ts | 4 +-
packages/cli/src/llm/types.ts | 2 +-
packages/cli/src/setup-models.ts | 108 +++-
packages/cli/src/status-project.ts | 64 ++-
.../ingest/local-bundle-runtime.test.ts | 5 +-
.../context/llm/codex-exec-events.test.ts | 188 +++++++
.../test/context/llm/codex-isolation.test.ts | 19 +
.../llm/codex-mcp-runtime-server.test.ts | 73 +++
.../cli/test/context/llm/codex-models.test.ts | 17 +
.../context/llm/codex-runtime-config.test.ts | 43 ++
.../test/context/llm/codex-runtime.test.ts | 460 ++++++++++++++++++
.../test/context/llm/codex-sdk-runner.test.ts | 97 ++++
.../context/llm/runtime-local-config.test.ts | 21 +
.../cli/test/context/project/config.test.ts | 27 +-
packages/cli/test/doctor.test.ts | 8 +
packages/cli/test/ingest.test.ts | 7 +-
packages/cli/test/llm/model-provider.test.ts | 9 +
packages/cli/test/setup-models.test.ts | 81 +++
packages/cli/test/status-project.test.ts | 131 +++++
pnpm-lock.yaml | 79 +++
scripts/codex-backend-live-smoke.mjs | 160 ++++++
scripts/codex-backend-live-smoke.test.mjs | 18 +
41 files changed, 2544 insertions(+), 30 deletions(-)
create mode 100644 packages/cli/src/context/llm/codex-exec-events.ts
create mode 100644 packages/cli/src/context/llm/codex-isolation.ts
create mode 100644 packages/cli/src/context/llm/codex-mcp-runtime-server.ts
create mode 100644 packages/cli/src/context/llm/codex-models.ts
create mode 100644 packages/cli/src/context/llm/codex-runtime-config.ts
create mode 100644 packages/cli/src/context/llm/codex-runtime.ts
create mode 100644 packages/cli/src/context/llm/codex-sdk-runner.ts
create mode 100644 packages/cli/test/context/llm/codex-exec-events.test.ts
create mode 100644 packages/cli/test/context/llm/codex-isolation.test.ts
create mode 100644 packages/cli/test/context/llm/codex-mcp-runtime-server.test.ts
create mode 100644 packages/cli/test/context/llm/codex-models.test.ts
create mode 100644 packages/cli/test/context/llm/codex-runtime-config.test.ts
create mode 100644 packages/cli/test/context/llm/codex-runtime.test.ts
create mode 100644 packages/cli/test/context/llm/codex-sdk-runner.test.ts
create mode 100644 scripts/codex-backend-live-smoke.mjs
create mode 100644 scripts/codex-backend-live-smoke.test.mjs
diff --git a/README.md b/README.md
index d44905d5..2c433e0d 100644
--- a/README.md
+++ b/README.md
@@ -30,8 +30,9 @@ warehouse accurately - from approved metric definitions, joinable columns, and
business knowledge it builds and maintains for you.
> [!NOTE]
-> Run **ktx** with your own LLM API keys or a **Claude Pro/Max** subscription.
-> No extra usage billing from **ktx**.
+> Run **ktx** with your own LLM API keys or a local agent sign-in — a
+> **Claude Pro/Max** subscription through Claude Code, or your local Codex
+> authentication. No extra usage billing from **ktx**.
@@ -175,8 +176,9 @@ then the current directory. Pass `--project-dir ` when scripting.
No. **ktx** runs locally. The only data leaving your machine is what you
send to the LLM provider you configured.
- **Which LLM backends are supported?**
- Anthropic API, Google Vertex AI, AI Gateway, and the local Claude Code
- session through the Claude Agent SDK. See
+ Anthropic API, Google Vertex AI, AI Gateway, the local Claude Code session
+ through the Claude Agent SDK, and your local Codex authentication through the
+ Codex SDK. See
[LLM configuration](https://docs.kaelio.com/ktx/docs/guides/llm-configuration).
- **How is ktx different from a dbt or MetricFlow semantic layer?**
**ktx** *ingests* those layers and combines them with raw-table
diff --git a/docs-site/content/docs/cli-reference/ktx-setup.mdx b/docs-site/content/docs/cli-reference/ktx-setup.mdx
index 0da7b339..24469a63 100644
--- a/docs-site/content/docs/cli-reference/ktx-setup.mdx
+++ b/docs-site/content/docs/cli-reference/ktx-setup.mdx
@@ -51,8 +51,9 @@ prompts.
| Flag | Description |
|------|-------------|
-| `--llm-backend ` | LLM backend: `anthropic`, `vertex`, or `claude-code` |
+| `--llm-backend ` | LLM backend: `anthropic`, `vertex`, `claude-code`, or `codex` |
| `--llm-backend claude-code` | Use the local Claude Code session for **ktx** LLM calls |
+| `--llm-backend codex` | Use local Codex authentication for **ktx** LLM calls |
| `--llm-model ` | LLM model ID or backend model alias to validate and save |
| `--anthropic-api-key-env ` | Environment variable containing the Anthropic API key |
| `--anthropic-api-key-file ` | File containing the Anthropic API key |
@@ -62,9 +63,14 @@ prompts.
Choose only one Anthropic credential source. Anthropic credential flags are only
valid with the Anthropic backend; Vertex flags are only valid with the Vertex
-backend. The `claude-code` backend uses local Claude Code authentication instead
+backend. The `claude-code` and `codex` backends use local authentication instead
of Anthropic API key or Vertex flags. For Claude Code, `--llm-model` accepts
-`sonnet`, `opus`, `haiku`, or a full Claude model ID.
+`sonnet`, `opus`, `haiku`, or a full Claude model ID. For Codex, `--llm-model`
+accepts `codex`, `default`, or a `gpt-*` / `codex-*` model ID such as
+`gpt-5.5`; any other value is rejected before the auth probe. Run `codex` to
+see the models available to your login, and pick a `gpt-*` / `codex-*` id from
+that list. Note that `*-codex` API-billing model IDs (for example
+`gpt-5.3-codex`) are not available to ChatGPT-subscription logins.
### Embeddings
@@ -191,6 +197,17 @@ ktx setup \
--llm-backend claude-code \
--llm-model opus
+# Configure **ktx** to use local Codex authentication for LLM work
+ktx setup --llm-backend codex --llm-model gpt-5.5 --no-input
+```
+
+When you choose `--llm-backend codex`, setup prints a warning if the public
+Codex SDK and CLI surface cannot prove full Claude-Code-style isolation. The
+backend restricts **ktx** runtime MCP tools to each run, but Codex may still
+load user Codex config and built-in command execution or read-only file
+capabilities.
+
+```bash
# Script a Postgres connection that reads its URL from the environment
ktx setup \
--project-dir ./analytics \
diff --git a/docs-site/content/docs/cli-reference/ktx-status.mdx b/docs-site/content/docs/cli-reference/ktx-status.mdx
index 51c00148..66e4964c 100644
--- a/docs-site/content/docs/cli-reference/ktx-status.mdx
+++ b/docs-site/content/docs/cli-reference/ktx-status.mdx
@@ -21,7 +21,7 @@ ktx status [options]
| `--json` | Print JSON output | `false` |
| `-v`, `--verbose` | Show every check, including passing ones | `false` |
| `--validate` | Only validate the `ktx.yaml` schema; skip readiness checks | `false` |
-| `--fast` | Skip checks that require external communication (query-history readiness probes and Claude Code auth probe) | `false` |
+| `--fast` | Skip checks that require external communication (query-history readiness probes, Claude Code auth probe, and Codex auth probe) | `false` |
| `--no-input` | Disable interactive terminal input | - |
## Examples
@@ -39,7 +39,7 @@ ktx status --verbose
# Validate ktx.yaml without running readiness checks
ktx status --validate
-# Skip slow probes (query-history readiness, Claude Code auth)
+# Skip slow probes (query-history readiness, Claude Code auth, Codex auth)
ktx status --fast
# Check a project from another directory
@@ -57,6 +57,16 @@ flow, then rerun `ktx status`. Use `--fast` to skip this probe (useful in CI
or offline contexts); skipped checks render as `-` and carry
`"status": "skipped"` in JSON output.
+For `llm.provider.backend: codex`, `ktx status` runs a minimal non-interactive
+Codex request. If the probe fails, authenticate Codex locally with the Codex CLI
+and verify the Codex CLI installation.
+
+When `llm.provider.backend: codex` is configured, `ktx status` also prints a
+warning when the installed public Codex SDK and CLI surface cannot prove full
+Claude-Code-style isolation. The warning does not block authenticated Codex
+usage, but it marks the project status as partial so you can make an explicit
+runtime-isolation decision.
+
A `Local data` section summarises what the project has accumulated locally:
ingest run counts, last completed timestamp per connection, knowledge page
counts by scope, semantic-layer source and dictionary value counts, and the
diff --git a/docs-site/content/docs/configuration/ktx-yaml.mdx b/docs-site/content/docs/configuration/ktx-yaml.mdx
index 13105851..a9298443 100644
--- a/docs-site/content/docs/configuration/ktx-yaml.mdx
+++ b/docs-site/content/docs/configuration/ktx-yaml.mdx
@@ -376,13 +376,23 @@ llm:
| Field | Type | Default | Purpose |
|-------|------|---------|---------|
-| `provider.backend` | `none` \| `anthropic` \| `vertex` \| `gateway` \| `claude-code` | `none` | Selected backend. `none` disables LLM features. `claude-code` uses the local Claude Code session and needs no API key. |
+| `provider.backend` | `none` \| `anthropic` \| `vertex` \| `gateway` \| `claude-code` \| `codex` | `none` | Selected backend. `none` disables LLM features. `claude-code` uses the local Claude Code session and needs no API key. `codex` uses local Codex authentication and needs no API key. |
| `provider.anthropic.api_key` | `string` | - | Anthropic API key. Required when `backend: anthropic`. Accepts `env:` or `file:` references. |
| `provider.anthropic.base_url` | `string` | - | Override the Anthropic API base URL (proxy, self-hosted gateway). |
| `provider.gateway.api_key` / `base_url` | `string` | - | Credentials for an AI Gateway provider. Required when `backend: gateway`. |
| `provider.vertex.project` | `string` | - | Google Cloud project ID hosting the Vertex AI endpoint. |
| `provider.vertex.location` | `string` | - | Vertex AI region (for example `us-east5`). Required when the `vertex` block is present. |
+Use `codex` when local Codex authentication should power **ktx** LLM work:
+
+```yaml
+llm:
+ provider:
+ backend: codex
+ models:
+ default: gpt-5.5
+```
+
### Model roles
`models` overrides the per-role model. Keys are fixed; values are
diff --git a/docs-site/content/docs/guides/building-context.mdx b/docs-site/content/docs/guides/building-context.mdx
index b806c424..52179e70 100644
--- a/docs-site/content/docs/guides/building-context.mdx
+++ b/docs-site/content/docs/guides/building-context.mdx
@@ -39,8 +39,20 @@ ktx ingest --all
Enriched ingest needs a configured model and embeddings. Run `ktx setup` first;
connections without that configuration fail before any work starts.
-With `claude-code`, **ktx** agent loops can invoke only the **ktx** MCP tools for the
-current run.
+Local-auth backends keep provider credentials out of `ktx.yaml`:
+
+```bash
+ktx setup --llm-backend claude-code --no-input
+ktx setup --llm-backend codex --llm-model gpt-5.5 --no-input
+```
+
+With `claude-code`, **ktx** agent loops can invoke only the **ktx** MCP tools
+for the current run. With `codex`, **ktx** restricts the temporary runtime MCP
+server to the current run's tool set, disables Codex web search, requests a
+read-only sandbox, and sets `approval_policy=never`. The public Codex SDK and
+CLI surface may still load user Codex config and built-in command execution or
+read-only file capabilities, so use `claude-code` for stricter runtime tool
+isolation.
## Query history
diff --git a/docs-site/content/docs/guides/llm-configuration.mdx b/docs-site/content/docs/guides/llm-configuration.mdx
index 880df24e..71ab9d80 100644
--- a/docs-site/content/docs/guides/llm-configuration.mdx
+++ b/docs-site/content/docs/guides/llm-configuration.mdx
@@ -16,6 +16,7 @@ Set `llm.provider.backend` to one of these values:
- `gateway`: Use AI Gateway-compatible Anthropic model ids.
- `claude-code`: Use your local Claude Code session through the Claude Agent
SDK. **ktx** strips provider-routing environment variables from child processes.
+- `codex`: Use your local Codex authentication through the Codex SDK.
## Claude Code
@@ -47,6 +48,42 @@ model IDs are also accepted.
metadata may still list host slash commands, skills, and subagents; **ktx** does not
grant execution access to them.
+## Codex backend
+
+Use `codex` when you want **ktx** to run LLM-backed workflows through your
+local Codex authentication instead of a direct provider API key.
+
+```yaml
+llm:
+ provider:
+ backend: codex
+ models:
+ default: gpt-5.5
+```
+
+Configure it non-interactively:
+
+```bash
+ktx setup --llm-backend codex --llm-model gpt-5.5 --no-input
+```
+
+This is separate from Codex agent-client setup. `ktx setup --agents --target
+codex` installs instructions and MCP access for an end-user Codex session.
+`ktx setup --llm-backend codex` makes **ktx** itself execute ingest, scan
+enrichment, memory, and other LLM-backed work through Codex.
+
+During runtime loops, **ktx** starts a temporary loopback MCP server for the
+current run, exposes only the tools passed to that run, asks Codex to use a
+read-only sandbox, sets `approval_policy=never`, auto-approves only those
+run-scoped MCP tools, and disables Codex web search.
+
+Codex backend isolation is currently limited by the public Codex SDK and CLI
+surface. Codex may still load user Codex config and built-in command execution
+or read-only file capabilities. Use `llm.provider.backend: claude-code` when
+you need stricter Claude-Code-style runtime tool isolation, or remove host
+Codex MCP and tool config before running untrusted prompts through the `codex`
+backend.
+
## Prompt caching
`llm.promptCaching` has partial parity on `claude-code`. Status and doctor warn
diff --git a/knip.json b/knip.json
index 270c2310..65b1a0a2 100644
--- a/knip.json
+++ b/knip.json
@@ -37,6 +37,9 @@
"@semantic-release/release-notes-generator",
"conventional-changelog-conventionalcommits"
],
+ "ignore": [
+ ".context/**"
+ ],
"ignoreBinaries": [
"uv",
"lsof"
diff --git a/package.json b/package.json
index fee7b745..a9590d70 100644
--- a/package.json
+++ b/package.json
@@ -32,6 +32,7 @@
"setup:dev": "node scripts/setup-dev.mjs",
"release:published-smoke": "node scripts/published-package-smoke.mjs --require-config",
"release:local-embeddings-smoke": "node scripts/local-embeddings-runtime-smoke.mjs --require-opt-in",
+ "release:codex-backend-smoke": "node scripts/codex-backend-live-smoke.mjs",
"release:readiness": "node scripts/release-readiness.mjs",
"release:update-version": "node scripts/update-public-release-version.mjs",
"relationships:acquire-public-fixtures": "node scripts/acquire-public-benchmark-fixtures.mjs",
diff --git a/packages/cli/package.json b/packages/cli/package.json
index b04fceac..9d3af54c 100644
--- a/packages/cli/package.json
+++ b/packages/cli/package.json
@@ -56,6 +56,7 @@
"@looker/sdk-rtl": "^21.6.5",
"@modelcontextprotocol/sdk": "^1.29.0",
"@notionhq/client": "^5.22.0",
+ "@openai/codex-sdk": "^0.133.0",
"ai": "^6.0.188",
"better-sqlite3": "^12.10.0",
"commander": "14.0.3",
diff --git a/packages/cli/src/commands/setup-commands.ts b/packages/cli/src/commands/setup-commands.ts
index 19f980bd..1619a80a 100644
--- a/packages/cli/src/commands/setup-commands.ts
+++ b/packages/cli/src/commands/setup-commands.ts
@@ -29,7 +29,7 @@ function embeddingBackend(value: string): 'openai' | 'sentence-transformers' {
}
function llmBackend(value: string): KtxSetupLlmBackend {
- if (value === 'anthropic' || value === 'vertex' || value === 'claude-code') {
+ if (value === 'anthropic' || value === 'vertex' || value === 'claude-code' || value === 'codex') {
return value;
}
throw new InvalidArgumentError(`invalid choice '${value}'`);
diff --git a/packages/cli/src/context/ingest/local-bundle-runtime.ts b/packages/cli/src/context/ingest/local-bundle-runtime.ts
index 77f4234e..9d6aba95 100644
--- a/packages/cli/src/context/ingest/local-bundle-runtime.ts
+++ b/packages/cli/src/context/ingest/local-bundle-runtime.ts
@@ -611,9 +611,10 @@ function nextLocalJobId(): string {
function localIngestLlmProviderGuardMessage(projectDir: string): string {
return [
- 'ktx ingest requires llm.provider.backend: anthropic, vertex, gateway, or claude-code, or an injected agentRunner.',
- 'Configure a local Claude Code session or API-backed LLM, then rerun ingest:',
+ 'ktx ingest requires llm.provider.backend: anthropic, vertex, gateway, claude-code, or codex, or an injected agentRunner.',
+ 'Configure a local Claude Code/Codex session or API-backed LLM, then rerun ingest:',
` ktx setup --project-dir ${projectDir} --llm-backend claude-code --no-input`,
+ ` ktx setup --project-dir ${projectDir} --llm-backend codex --llm-model gpt-5.5 --no-input`,
` ktx setup --project-dir ${projectDir} --llm-backend anthropic --anthropic-api-key-env ANTHROPIC_API_KEY --llm-model claude-sonnet-4-6 --no-input`,
].join('\n');
}
diff --git a/packages/cli/src/context/llm/codex-exec-events.ts b/packages/cli/src/context/llm/codex-exec-events.ts
new file mode 100644
index 00000000..86e13694
--- /dev/null
+++ b/packages/cli/src/context/llm/codex-exec-events.ts
@@ -0,0 +1,194 @@
+import type { LlmTokenUsage, RunLoopStopReason } from './runtime-port.js';
+
+export interface CodexExecEventSummary {
+ finalText: string;
+ stopReason: RunLoopStopReason;
+ usage: LlmTokenUsage;
+ stepCount: number;
+ stepBoundariesMs: number[];
+ toolCallCount: number;
+ toolFailures: string[];
+ error?: Error;
+}
+
+interface CodexEventParseOptions {
+ startedAt?: number;
+ now?: () => number;
+}
+
+function record(value: unknown): Record | undefined {
+ return value && typeof value === 'object' ? (value as Record) : undefined;
+}
+
+/**
+ * Codex thread items that represent a discrete agent action consuming one loop
+ * step. The step budget caps the total number of these regardless of which
+ * capability the agent reaches for, so built-in `command_execution` (and any
+ * file/web action the public Codex surface still exposes) count alongside our
+ * own `mcp_tool_call` items rather than only the MCP ones.
+ */
+const AGENT_STEP_ITEM_TYPES = new Set(['command_execution', 'mcp_tool_call', 'file_change', 'web_search']);
+
+export function isCompletedAgentStep(event: unknown): boolean {
+ const eventRecord = record(event);
+ if (eventRecord?.type !== 'item.completed') {
+ return false;
+ }
+ const itemType = record(eventRecord.item)?.type;
+ return typeof itemType === 'string' && AGENT_STEP_ITEM_TYPES.has(itemType);
+}
+
+function text(value: unknown): string | undefined {
+ return typeof value === 'string' && value.trim().length > 0 ? value : undefined;
+}
+
+function numberValue(value: unknown): number | undefined {
+ return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
+}
+
+function usageFrom(value: unknown): LlmTokenUsage {
+ const usage = record(value);
+ if (!usage) {
+ return {};
+ }
+ const inputTokens = numberValue(usage.input_tokens ?? usage.inputTokens);
+ const outputTokens = numberValue(usage.output_tokens ?? usage.outputTokens);
+ const explicitTotalTokens = numberValue(usage.total_tokens ?? usage.totalTokens);
+ const totalTokens =
+ explicitTotalTokens ??
+ (inputTokens !== undefined && outputTokens !== undefined ? inputTokens + outputTokens : undefined);
+ return {
+ ...(inputTokens !== undefined ? { inputTokens } : {}),
+ ...(outputTokens !== undefined ? { outputTokens } : {}),
+ ...(totalTokens !== undefined ? { totalTokens } : {}),
+ };
+}
+
+function stopReasonFrom(value: unknown): RunLoopStopReason {
+ const reason = text(value)?.toLowerCase();
+ if (reason && /(budget|max_turn|max-turn|limit)/.test(reason)) {
+ return 'budget';
+ }
+ return 'natural';
+}
+
+function errorMessageFrom(value: unknown): string {
+ if (value instanceof Error) {
+ return value.message;
+ }
+ const asRecord = record(value);
+ const message = text(asRecord?.message);
+ return message ?? text(value) ?? 'Codex turn failed';
+}
+
+/**
+ * Codex serializes API failures as a JSON envelope inside the event message
+ * (e.g. `{"type":"error","status":400,"error":{"message":"…"}}`). Surface the
+ * human-readable inner message so callers don't leak raw JSON; pass plain
+ * strings through unchanged.
+ */
+function unwrapCodexApiErrorMessage(raw: string): string {
+ const trimmed = raw.trim();
+ if (!trimmed.startsWith('{')) {
+ return raw;
+ }
+ try {
+ const parsed = record(JSON.parse(trimmed));
+ return text(record(parsed?.error)?.message) ?? text(parsed?.message) ?? raw;
+ } catch {
+ return raw;
+ }
+}
+
+/** @internal */
+export function parseCodexExecEventLine(line: string): unknown {
+ try {
+ return JSON.parse(line) as unknown;
+ } catch (error) {
+ throw new Error(`Codex JSONL event stream was malformed: ${error instanceof Error ? error.message : String(error)}`);
+ }
+}
+
+export function summarizeCodexExecEvents(
+ events: Iterable,
+ options: CodexEventParseOptions = {},
+): CodexExecEventSummary {
+ const startedAt = options.startedAt ?? Date.now();
+ const now = options.now ?? Date.now;
+ let finalText = '';
+ let stopReason: RunLoopStopReason = 'natural';
+ let usage: LlmTokenUsage = {};
+ let turnCount = 0;
+ let completedStepCount = 0;
+ const stepBoundariesMs: number[] = [];
+ let toolCallCount = 0;
+ const toolFailures: string[] = [];
+ let error: Error | undefined;
+
+ for (const event of events) {
+ const eventRecord = record(event);
+ const eventType = text(eventRecord?.type);
+ if (!eventRecord || !eventType) {
+ continue;
+ }
+
+ if (eventType === 'turn.started') {
+ turnCount += 1;
+ continue;
+ }
+
+ const item = record(eventRecord.item);
+ const itemType = text(item?.type);
+
+ if (eventType === 'item.started' && itemType === 'mcp_tool_call') {
+ toolCallCount += 1;
+ continue;
+ }
+
+ if (isCompletedAgentStep(event)) {
+ completedStepCount += 1;
+ stepBoundariesMs.push(now() - startedAt);
+ // Only MCP tool calls fail the loop: a non-zero `command_execution` exit
+ // is normal agent exploration, not a runtime error. `status` is the
+ // authoritative signal (the SDK always sets it); the SDK also serializes
+ // `error: null` on successful calls, so an explicit-null `error` must NOT
+ // be read as a failure — only a populated error object counts.
+ if (itemType === 'mcp_tool_call' && (item?.status === 'failed' || (item?.error !== undefined && item?.error !== null))) {
+ const name = text(item?.name) ?? text(item?.tool) ?? text(item?.tool_name) ?? 'unknown';
+ toolFailures.push(`${name}: ${errorMessageFrom(item?.error)}`);
+ }
+ continue;
+ }
+
+ if (eventType === 'item.completed' && itemType === 'agent_message') {
+ finalText = text(item?.text) ?? finalText;
+ continue;
+ }
+
+ if (eventType === 'turn.completed') {
+ usage = usageFrom(eventRecord.usage);
+ if (completedStepCount === 0) {
+ stepBoundariesMs.push(now() - startedAt);
+ }
+ stopReason = stopReasonFrom(eventRecord.reason ?? eventRecord.stop_reason ?? eventRecord.terminal_reason);
+ continue;
+ }
+
+ if (eventType === 'turn.failed' || eventType === 'error') {
+ stopReason = 'error';
+ error = new Error(unwrapCodexApiErrorMessage(errorMessageFrom(eventRecord.error ?? eventRecord.message)));
+ continue;
+ }
+ }
+
+ return {
+ finalText,
+ stopReason,
+ usage,
+ stepCount: completedStepCount > 0 ? completedStepCount : turnCount,
+ stepBoundariesMs,
+ toolCallCount,
+ toolFailures,
+ ...(error ? { error } : {}),
+ };
+}
diff --git a/packages/cli/src/context/llm/codex-isolation.ts b/packages/cli/src/context/llm/codex-isolation.ts
new file mode 100644
index 00000000..d54ac1f8
--- /dev/null
+++ b/packages/cli/src/context/llm/codex-isolation.ts
@@ -0,0 +1,9 @@
+export const CODEX_ISOLATION_WARNING =
+ 'Codex backend isolation is limited by the public Codex SDK/CLI surface: ktx restricts the runtime MCP server to the current ktx tool set, disables Codex web search, asks for a read-only sandbox, and sets approval_policy=never, but Codex may still load user Codex config and built-in command execution or read-only file capabilities.';
+
+export const CODEX_ISOLATION_WARNING_FIX =
+ 'Use llm.provider.backend: claude-code when you need stricter Claude-Code-style runtime tool isolation, or remove host Codex MCP/tool config before running untrusted prompts through the codex backend.';
+
+export function formatCodexIsolationWarning(): string {
+ return `${CODEX_ISOLATION_WARNING} ${CODEX_ISOLATION_WARNING_FIX}`;
+}
diff --git a/packages/cli/src/context/llm/codex-mcp-runtime-server.ts b/packages/cli/src/context/llm/codex-mcp-runtime-server.ts
new file mode 100644
index 00000000..eacf28f9
--- /dev/null
+++ b/packages/cli/src/context/llm/codex-mcp-runtime-server.ts
@@ -0,0 +1,87 @@
+import { randomBytes } from 'node:crypto';
+import type { Server } from 'node:http';
+import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
+import type { KtxMcpServerLike } from '../mcp/types.js';
+import { runKtxMcpHttpServer, type KtxMcpHttpServerHandle } from '../../mcp-http-server.js';
+import type { KtxRuntimeToolSet } from './runtime-port.js';
+import { normalizeKtxRuntimeToolOutput } from './runtime-tools.js';
+
+/** @internal */
+export interface CreateCodexRuntimeMcpServerInput {
+ server?: KtxMcpServerLike;
+ toolSet: KtxRuntimeToolSet;
+}
+
+export interface CodexRuntimeMcpServerHandle {
+ url: string;
+ bearerTokenEnvVar: 'KTX_CODEX_RUNTIME_MCP_TOKEN';
+ bearerToken: string;
+ close(): Promise;
+}
+
+type RunServer = typeof runKtxMcpHttpServer;
+
+export interface StartCodexRuntimeMcpServerInput {
+ projectDir: string;
+ toolSet: KtxRuntimeToolSet;
+ runServer?: RunServer;
+}
+
+/** @internal */
+export function createCodexRuntimeMcpServer(input: CreateCodexRuntimeMcpServerInput): KtxMcpServerLike {
+ const server =
+ input.server ??
+ (new McpServer({
+ name: 'ktx-runtime',
+ version: '0.0.0',
+ }) as KtxMcpServerLike);
+
+ for (const descriptor of Object.values(input.toolSet)) {
+ server.registerTool(
+ descriptor.name,
+ {
+ description: descriptor.description,
+ inputSchema: descriptor.inputSchema.shape,
+ },
+ async (toolInput) => {
+ const normalized = normalizeKtxRuntimeToolOutput(await descriptor.execute(toolInput));
+ return {
+ content: [{ type: 'text', text: normalized.markdown }],
+ ...(normalized.structured !== undefined && normalized.structured !== null && typeof normalized.structured === 'object'
+ ? { structuredContent: normalized.structured as object }
+ : {}),
+ };
+ },
+ );
+ }
+
+ return server;
+}
+
+function serverPort(server: Server, fallback: number): number {
+ const address = server.address();
+ return typeof address === 'object' && address ? address.port : fallback;
+}
+
+export async function startCodexRuntimeMcpServer(
+ input: StartCodexRuntimeMcpServerInput,
+): Promise {
+ const bearerToken = randomBytes(32).toString('hex');
+ const runServer = input.runServer ?? runKtxMcpHttpServer;
+ const handle = (await runServer({
+ projectDir: input.projectDir,
+ host: '127.0.0.1',
+ port: 0,
+ token: bearerToken,
+ allowedHosts: ['127.0.0.1', 'localhost'],
+ allowedOrigins: [],
+ createMcpServer: () => createCodexRuntimeMcpServer({ toolSet: input.toolSet }) as McpServer,
+ })) as KtxMcpHttpServerHandle;
+ const port = serverPort(handle.server, 0);
+ return {
+ url: `http://127.0.0.1:${port}/mcp`,
+ bearerTokenEnvVar: 'KTX_CODEX_RUNTIME_MCP_TOKEN',
+ bearerToken,
+ close: () => handle.close(),
+ };
+}
diff --git a/packages/cli/src/context/llm/codex-models.ts b/packages/cli/src/context/llm/codex-models.ts
new file mode 100644
index 00000000..1a8b9b9d
--- /dev/null
+++ b/packages/cli/src/context/llm/codex-models.ts
@@ -0,0 +1,20 @@
+export const DEFAULT_CODEX_MODEL = 'gpt-5.5';
+
+const CODEX_MODEL_ALIASES: Record = {
+ codex: DEFAULT_CODEX_MODEL,
+ default: DEFAULT_CODEX_MODEL,
+};
+
+const EXPLICIT_CODEX_MODEL_ID = /^(?:gpt|codex)-[a-z0-9][a-z0-9._-]*$/i;
+
+export function resolveCodexModel(model: string): string {
+ const normalized = model.trim();
+ const alias = CODEX_MODEL_ALIASES[normalized];
+ if (alias) {
+ return alias;
+ }
+ if (EXPLICIT_CODEX_MODEL_ID.test(normalized)) {
+ return normalized;
+ }
+ throw new Error(`Unsupported Codex model "${model}". Use codex, default, or a gpt-* / codex-* model id.`);
+}
diff --git a/packages/cli/src/context/llm/codex-runtime-config.ts b/packages/cli/src/context/llm/codex-runtime-config.ts
new file mode 100644
index 00000000..74de9efe
--- /dev/null
+++ b/packages/cli/src/context/llm/codex-runtime-config.ts
@@ -0,0 +1,38 @@
+interface CodexRuntimeMcpConfig {
+ url: string;
+ bearerTokenEnvVar: string;
+ bearerToken: string;
+ toolNames: string[];
+}
+
+export interface BuildCodexRuntimeConfigInput {
+ model: string;
+ mcp?: CodexRuntimeMcpConfig;
+}
+
+export interface CodexRuntimeConfig {
+ configOverrides: Record;
+ env: Record;
+}
+
+export function buildCodexRuntimeConfig(input: BuildCodexRuntimeConfigInput): CodexRuntimeConfig {
+ const configOverrides: Record = {
+ history: { persistence: 'none' },
+ };
+ const env: Record = {};
+
+ if (input.mcp) {
+ configOverrides.mcp_servers = {
+ ktx: {
+ url: input.mcp.url,
+ bearer_token_env_var: input.mcp.bearerTokenEnvVar,
+ enabled_tools: input.mcp.toolNames,
+ default_tools_approval_mode: 'approve',
+ required: true,
+ },
+ };
+ env[input.mcp.bearerTokenEnvVar] = input.mcp.bearerToken;
+ }
+
+ return { configOverrides, env };
+}
diff --git a/packages/cli/src/context/llm/codex-runtime.ts b/packages/cli/src/context/llm/codex-runtime.ts
new file mode 100644
index 00000000..3535072b
--- /dev/null
+++ b/packages/cli/src/context/llm/codex-runtime.ts
@@ -0,0 +1,371 @@
+import { z } from 'zod';
+import { noopLogger, type KtxLogger } from '../core/config.js';
+import { isCompletedAgentStep, summarizeCodexExecEvents, type CodexExecEventSummary } from './codex-exec-events.js';
+import {
+ startCodexRuntimeMcpServer,
+ type CodexRuntimeMcpServerHandle,
+} from './codex-mcp-runtime-server.js';
+import { resolveCodexModel } from './codex-models.js';
+import { buildCodexRuntimeConfig } from './codex-runtime-config.js';
+import { CodexSdkCliRunner, type CodexSdkRunner } from './codex-sdk-runner.js';
+import type {
+ KtxGenerateObjectInput,
+ KtxGenerateTextInput,
+ KtxLlmRuntimePort,
+ KtxRuntimeToolSet,
+ LlmTokenUsage,
+ RunLoopParams,
+ RunLoopResult,
+} from './runtime-port.js';
+
+export interface CodexKtxLlmRuntimeDeps {
+ projectDir: string;
+ modelSlots: { default: string } & Partial>;
+ runner?: CodexSdkRunner;
+ startMcpServer?: (input: { projectDir: string; toolSet: KtxRuntimeToolSet }) => Promise;
+ logger?: KtxLogger;
+}
+
+function modelForRole(modelSlots: CodexKtxLlmRuntimeDeps['modelSlots'], role: string): string {
+ return resolveCodexModel(modelSlots[role] ?? modelSlots.default);
+}
+
+function promptWithSystem(system: string | undefined, prompt: string): string {
+ return [system, prompt].filter(Boolean).join('\n\n');
+}
+
+interface CollectCodexEventsOptions {
+ stepBudget?: number;
+ abortController?: AbortController;
+ onStep?: (stepIndex: number) => void | Promise;
+}
+
+interface CollectCodexEventsResult {
+ events: unknown[];
+ budgetExceeded: boolean;
+ streamError?: Error;
+}
+
+function eventRecord(value: unknown): Record | undefined {
+ return value && typeof value === 'object' ? (value as Record) : undefined;
+}
+
+function isTurnCompleted(event: unknown): boolean {
+ return eventRecord(event)?.type === 'turn.completed';
+}
+
+/**
+ * Drains the Codex stream once, emitting a step as each agent action completes
+ * so callers see live progress and the step budget is enforced mid-run. Every
+ * completed agent-action item counts (see {@link isCompletedAgentStep}), so
+ * built-in `command_execution` steps decrement the budget the same as
+ * `mcp_tool_call`s. A turn that produced no actions still counts as one step,
+ * matching the metrics summary and the AI SDK backend.
+ */
+async function collectEvents(
+ events: AsyncIterable,
+ options: CollectCodexEventsOptions = {},
+): Promise {
+ const collected: unknown[] = [];
+ let completedSteps = 0;
+ let sawActionStep = false;
+ let budgetExceeded = false;
+ let streamError: Error | undefined;
+
+ // The SDK yields every stdout event, then throws on a non-zero codex exec
+ // exit. Catch that throw so the events already collected (which carry the
+ // real `turn.failed`/`error` reason) survive for the summary; the masked
+ // exit message is kept only as a fallback when no error event was emitted.
+ try {
+ for await (const event of events) {
+ collected.push(event);
+
+ const isActionStep = isCompletedAgentStep(event);
+ if (isActionStep) {
+ sawActionStep = true;
+ } else if (sawActionStep || !isTurnCompleted(event)) {
+ // Only fall back to counting a bare turn as a step when the turn produced
+ // no agent actions; a completed turn is terminal, so it never aborts.
+ continue;
+ }
+
+ completedSteps += 1;
+ await options.onStep?.(completedSteps);
+ if (isActionStep && options.stepBudget !== undefined && completedSteps >= options.stepBudget) {
+ budgetExceeded = true;
+ options.abortController?.abort();
+ break;
+ }
+ }
+ } catch (error) {
+ streamError = error instanceof Error ? error : new Error(String(error));
+ }
+
+ return { events: collected, budgetExceeded, ...(streamError ? { streamError } : {}) };
+}
+
+function metrics(summary: CodexExecEventSummary, startedAt: number): { totalMs: number; usage: LlmTokenUsage } {
+ return { totalMs: Date.now() - startedAt, usage: summary.usage };
+}
+
+function summaryError(summary: CodexExecEventSummary, streamError?: Error): Error | undefined {
+ // A `turn.failed`/`error` event carries the real reason; prefer it over the
+ // SDK's generic non-zero-exit throw. Fall back to the stream error only when
+ // no event explained the failure (e.g. spawn failure or auth before a turn).
+ if (summary.error) {
+ return summary.error;
+ }
+ if (summary.toolFailures.length > 0) {
+ return new Error(`Codex runtime tool call failed: ${summary.toolFailures.join('; ')}`);
+ }
+ return streamError;
+}
+
+function assertSuccessfulText(summary: CodexExecEventSummary, streamError?: Error): string {
+ const error = summaryError(summary, streamError);
+ if (error) {
+ throw error;
+ }
+ if (!summary.finalText.trim()) {
+ throw new Error('Codex completed without an agent message');
+ }
+ return summary.finalText;
+}
+
+function parseStructuredOutput>(schema: TSchema, text: string): TOutput {
+ try {
+ return schema.parse(JSON.parse(text));
+ } catch (error) {
+ const message = error instanceof Error ? error.message : String(error);
+ throw new Error(`Codex structured output failed validation: ${message}`);
+ }
+}
+
+async function mcpForTools(input: {
+ projectDir: string;
+ toolSet?: KtxRuntimeToolSet;
+ startMcpServer: CodexKtxLlmRuntimeDeps['startMcpServer'];
+}): Promise {
+ if (!input.toolSet || Object.keys(input.toolSet).length === 0) {
+ return undefined;
+ }
+ return (input.startMcpServer ?? startCodexRuntimeMcpServer)({
+ projectDir: input.projectDir,
+ toolSet: input.toolSet,
+ });
+}
+
+function runtimeToolNames(toolSet: KtxRuntimeToolSet | undefined): string[] {
+ return Object.values(toolSet ?? {}).map((descriptor) => descriptor.name);
+}
+
+export class CodexKtxLlmRuntime implements KtxLlmRuntimePort {
+ private readonly runner: CodexSdkRunner;
+ private readonly logger: KtxLogger;
+
+ constructor(private readonly deps: CodexKtxLlmRuntimeDeps) {
+ this.runner = deps.runner ?? new CodexSdkCliRunner();
+ this.logger = deps.logger ?? noopLogger;
+ }
+
+ async generateText(input: KtxGenerateTextInput): Promise {
+ const startedAt = Date.now();
+ const model = modelForRole(this.deps.modelSlots, input.role);
+ const mcp = await mcpForTools({
+ projectDir: this.deps.projectDir,
+ toolSet: input.tools,
+ startMcpServer: this.deps.startMcpServer,
+ });
+ try {
+ const config = buildCodexRuntimeConfig({
+ model,
+ ...(mcp
+ ? {
+ mcp: {
+ url: mcp.url,
+ bearerTokenEnvVar: mcp.bearerTokenEnvVar,
+ bearerToken: mcp.bearerToken,
+ toolNames: runtimeToolNames(input.tools),
+ },
+ }
+ : {}),
+ });
+ const collected = await collectEvents(
+ await this.runner.runStreamed({
+ projectDir: this.deps.projectDir,
+ model,
+ prompt: promptWithSystem(input.system, input.prompt),
+ configOverrides: config.configOverrides,
+ env: config.env,
+ }),
+ );
+ const summary = summarizeCodexExecEvents(collected.events, { startedAt });
+ input.onMetrics?.(metrics(summary, startedAt));
+ return assertSuccessfulText(summary, collected.streamError);
+ } finally {
+ await mcp?.close();
+ }
+ }
+
+ async generateObject>(
+ input: KtxGenerateObjectInput,
+ ): Promise {
+ const startedAt = Date.now();
+ const model = modelForRole(this.deps.modelSlots, input.role);
+ const mcp = await mcpForTools({
+ projectDir: this.deps.projectDir,
+ toolSet: input.tools,
+ startMcpServer: this.deps.startMcpServer,
+ });
+ try {
+ const config = buildCodexRuntimeConfig({
+ model,
+ ...(mcp
+ ? {
+ mcp: {
+ url: mcp.url,
+ bearerTokenEnvVar: mcp.bearerTokenEnvVar,
+ bearerToken: mcp.bearerToken,
+ toolNames: runtimeToolNames(input.tools),
+ },
+ }
+ : {}),
+ });
+ const collected = await collectEvents(
+ await this.runner.runStreamed({
+ projectDir: this.deps.projectDir,
+ model,
+ prompt: promptWithSystem(input.system, input.prompt),
+ configOverrides: config.configOverrides,
+ env: config.env,
+ outputSchema: z.toJSONSchema(input.schema, { target: 'draft-7' }) as Record,
+ }),
+ );
+ const summary = summarizeCodexExecEvents(collected.events, { startedAt });
+ input.onMetrics?.(metrics(summary, startedAt));
+ return parseStructuredOutput(input.schema, assertSuccessfulText(summary, collected.streamError));
+ } finally {
+ await mcp?.close();
+ }
+ }
+
+ async runAgentLoop(params: RunLoopParams): Promise {
+ const startedAt = Date.now();
+ const model = modelForRole(this.deps.modelSlots, params.modelRole);
+ let mcp: CodexRuntimeMcpServerHandle | undefined;
+ try {
+ mcp = await mcpForTools({
+ projectDir: this.deps.projectDir,
+ toolSet: params.toolSet,
+ startMcpServer: this.deps.startMcpServer,
+ });
+ const config = buildCodexRuntimeConfig({
+ model,
+ ...(mcp
+ ? {
+ mcp: {
+ url: mcp.url,
+ bearerTokenEnvVar: mcp.bearerTokenEnvVar,
+ bearerToken: mcp.bearerToken,
+ toolNames: runtimeToolNames(params.toolSet),
+ },
+ }
+ : {}),
+ });
+ const abortController = new AbortController();
+ const onStep = async (stepIndex: number): Promise => {
+ try {
+ await params.onStepFinish?.({ stepIndex, stepBudget: params.stepBudget });
+ } catch (error) {
+ this.logger.warn(
+ `[codex-runner] onStepFinish callback threw; ignoring: ${error instanceof Error ? error.message : String(error)}`,
+ );
+ }
+ };
+ const collected = await collectEvents(
+ await this.runner.runStreamed({
+ projectDir: this.deps.projectDir,
+ model,
+ prompt: promptWithSystem(params.systemPrompt, params.userPrompt),
+ configOverrides: config.configOverrides,
+ env: config.env,
+ signal: abortController.signal,
+ }),
+ { stepBudget: params.stepBudget, abortController, onStep },
+ );
+ const summary = summarizeCodexExecEvents(collected.events, { startedAt });
+ const error = summaryError(summary, collected.streamError);
+ const stopReason = collected.budgetExceeded ? 'budget' : error ? 'error' : summary.stopReason;
+ return {
+ stopReason,
+ ...(stopReason === 'error' && error ? { error } : {}),
+ metrics: {
+ totalMs: Date.now() - startedAt,
+ usage: summary.usage,
+ stepCount: summary.stepCount,
+ stepBoundariesMs: summary.stepBoundariesMs,
+ },
+ };
+ } catch (error) {
+ const err = error instanceof Error ? error : new Error(String(error));
+ return {
+ stopReason: 'error',
+ error: err,
+ metrics: { totalMs: Date.now() - startedAt, usage: {}, stepCount: 0, stepBoundariesMs: [] },
+ };
+ } finally {
+ await mcp?.close();
+ }
+ }
+}
+
+// A rejected model is not an auth failure: Codex authenticated, connected, and
+// the API refused the model id. These markers come from the API error envelope
+// (e.g. "model is not supported", "invalid_request_error").
+const MODEL_UNAVAILABLE_MARKERS =
+ /\bnot supported\b|\bnot available\b|\bdoes not exist\b|invalid_request_error|\bunknown model\b|\bunsupported model\b/i;
+
+function describeCodexProbeFailure(model: string, message: string): { message: string; fix: string } {
+ if (MODEL_UNAVAILABLE_MARKERS.test(message)) {
+ const fix = `Run \`codex\` to see the models your account supports, then set llm.models.default in ktx.yaml (or rerun \`ktx setup\`).`;
+ return {
+ message: `Codex is authenticated, but the configured model "${model}" is not available for this Codex account. ${fix} Details: ${message}`,
+ fix,
+ };
+ }
+ const fix = `Authenticate Codex locally with the Codex CLI, verify the Codex CLI is installed, then rerun setup or \`ktx status\`.`;
+ return {
+ message: `Codex authentication is not usable. ${fix} Details: ${message}`,
+ fix,
+ };
+}
+
+export async function runCodexAuthProbe(input: {
+ projectDir: string;
+ model: string;
+ runner?: CodexSdkRunner;
+}): Promise<{ ok: true } | { ok: false; message: string; fix: string }> {
+ let model: string;
+ try {
+ model = resolveCodexModel(input.model);
+ } catch (error) {
+ return {
+ ok: false,
+ message: error instanceof Error ? error.message : String(error),
+ fix: 'Set llm.models.default in ktx.yaml to a supported codex model (codex, default, or a gpt-* / codex-* id), or rerun `ktx setup`.',
+ };
+ }
+
+ const runtime = new CodexKtxLlmRuntime({
+ projectDir: input.projectDir,
+ modelSlots: { default: model },
+ ...(input.runner ? { runner: input.runner } : {}),
+ });
+ try {
+ await runtime.generateText({ role: 'default', prompt: 'Reply with exactly: ok' });
+ return { ok: true };
+ } catch (error) {
+ const message = error instanceof Error ? error.message : String(error);
+ return { ok: false, ...describeCodexProbeFailure(model, message) };
+ }
+}
diff --git a/packages/cli/src/context/llm/codex-sdk-runner.ts b/packages/cli/src/context/llm/codex-sdk-runner.ts
new file mode 100644
index 00000000..58170b3a
--- /dev/null
+++ b/packages/cli/src/context/llm/codex-sdk-runner.ts
@@ -0,0 +1,96 @@
+import { Codex, type CodexOptions, type ThreadOptions, type TurnOptions } from '@openai/codex-sdk';
+
+export interface CodexSdkRunnerInput {
+ projectDir: string;
+ model: string;
+ prompt: string;
+ configOverrides?: Record;
+ env?: Record;
+ outputSchema?: Record;
+ signal?: AbortSignal;
+}
+
+export interface CodexSdkRunner {
+ runStreamed(input: CodexSdkRunnerInput): Promise>;
+}
+
+type CodexThread = {
+ runStreamed(input: string, turnOptions?: TurnOptions): Promise<{ events: AsyncIterable }>;
+};
+
+type CodexClient = {
+ startThread(options: ThreadOptions): CodexThread;
+};
+
+type CodexConstructor = new (options?: CodexOptions) => CodexClient;
+
+export interface CodexSdkCliRunnerOptions {
+ envBase?: NodeJS.ProcessEnv;
+ codexPathOverride?: string;
+}
+
+const CODEX_ENV_ALLOWLIST = new Set([
+ 'HOME',
+ 'USERPROFILE',
+ 'APPDATA',
+ 'LOCALAPPDATA',
+ 'XDG_CONFIG_HOME',
+ 'CODEX_HOME',
+ 'CODEX_API_KEY',
+ 'OPENAI_API_KEY',
+ 'PATH',
+ 'Path',
+ 'SYSTEMROOT',
+ 'COMSPEC',
+ 'TMPDIR',
+ 'TMP',
+ 'TEMP',
+ 'SSL_CERT_FILE',
+ 'SSL_CERT_DIR',
+ 'NODE_EXTRA_CA_CERTS',
+ 'HTTPS_PROXY',
+ 'HTTP_PROXY',
+ 'ALL_PROXY',
+ 'NO_PROXY',
+]);
+
+function buildCodexSdkEnv(baseEnv: NodeJS.ProcessEnv, overrides: Record | undefined): Record {
+ const env: Record = {};
+ for (const key of CODEX_ENV_ALLOWLIST) {
+ const value = baseEnv[key];
+ if (typeof value === 'string') {
+ env[key] = value;
+ }
+ }
+ return { ...env, ...(overrides ?? {}) };
+}
+
+export class CodexSdkCliRunner implements CodexSdkRunner {
+ constructor(private readonly options: CodexSdkCliRunnerOptions = {}) {}
+
+ async runStreamed(input: CodexSdkRunnerInput): Promise> {
+ const CodexClass = Codex as CodexConstructor;
+ const codex = new CodexClass({
+ ...(input.configOverrides ? { config: input.configOverrides as CodexOptions['config'] } : {}),
+ env: buildCodexSdkEnv(this.options.envBase ?? process.env, input.env),
+ ...(this.options.codexPathOverride ? { codexPathOverride: this.options.codexPathOverride } : {}),
+ });
+ const thread = codex.startThread({
+ workingDirectory: input.projectDir,
+ skipGitRepoCheck: true,
+ model: input.model,
+ sandboxMode: 'read-only',
+ webSearchMode: 'disabled',
+ approvalPolicy: 'never',
+ });
+ const turnOptions: TurnOptions = {
+ ...(input.outputSchema ? { outputSchema: input.outputSchema } : {}),
+ ...(input.signal ? { signal: input.signal } : {}),
+ };
+ const streamed = await thread.runStreamed(
+ input.prompt,
+ Object.keys(turnOptions).length > 0 ? turnOptions : undefined,
+ );
+ return streamed.events;
+ }
+}
diff --git a/packages/cli/src/context/llm/local-config.ts b/packages/cli/src/context/llm/local-config.ts
index c64a85cf..58bd29a5 100644
--- a/packages/cli/src/context/llm/local-config.ts
+++ b/packages/cli/src/context/llm/local-config.ts
@@ -5,6 +5,7 @@ import { resolveKtxConfigReference } from '../core/config-reference.js';
import type { KtxProjectEmbeddingConfig, KtxProjectLlmConfig } from '../project/config.js';
import { AiSdkKtxLlmRuntime } from './ai-sdk-runtime.js';
import { ClaudeCodeKtxLlmRuntime } from './claude-code-runtime.js';
+import { CodexKtxLlmRuntime } from './codex-runtime.js';
import type { KtxLlmRuntimePort } from './runtime-port.js';
interface LocalConfigDeps {
@@ -13,6 +14,7 @@ interface LocalConfigDeps {
createKtxLlmProvider?: typeof createKtxLlmProvider;
createKtxEmbeddingProvider?: typeof createKtxEmbeddingProvider;
createClaudeCodeRuntime?: (deps: ConstructorParameters[0]) => KtxLlmRuntimePort;
+ createCodexRuntime?: (deps: ConstructorParameters[0]) => KtxLlmRuntimePort;
createAiSdkRuntime?: (deps: { llmProvider: KtxLlmProvider }) => KtxLlmRuntimePort;
}
@@ -104,7 +106,7 @@ export function createLocalKtxLlmProviderFromConfig(
deps: LocalConfigDeps = {},
): KtxLlmProvider | null {
const resolved = resolveLocalKtxLlmConfig(config, deps.env ?? process.env);
- if (!resolved || resolved.backend === 'claude-code') {
+ if (!resolved || resolved.backend === 'claude-code' || resolved.backend === 'codex') {
return null;
}
return (deps.createKtxLlmProvider ?? createKtxLlmProvider)(resolved);
@@ -129,6 +131,16 @@ export function createLocalKtxLlmRuntimeFromConfig(
env: deps.env,
});
}
+ if (resolved.backend === 'codex') {
+ const projectDir = deps.projectDir;
+ if (!projectDir) {
+ throw new Error('projectDir is required when creating the codex LLM runtime');
+ }
+ return (deps.createCodexRuntime ?? ((runtimeDeps) => new CodexKtxLlmRuntime(runtimeDeps)))({
+ projectDir,
+ modelSlots: resolved.modelSlots,
+ });
+ }
const llmProvider = (deps.createKtxLlmProvider ?? createKtxLlmProvider)(resolved);
return (deps.createAiSdkRuntime ?? ((runtimeDeps) => new AiSdkKtxLlmRuntime(runtimeDeps)))({ llmProvider });
}
diff --git a/packages/cli/src/context/project/config.ts b/packages/cli/src/context/project/config.ts
index a8d38d1d..cbea79b6 100644
--- a/packages/cli/src/context/project/config.ts
+++ b/packages/cli/src/context/project/config.ts
@@ -3,7 +3,7 @@ import YAML from 'yaml';
import * as z from 'zod';
import { connectionConfigSchema } from './driver-schemas.js';
-const KTX_LLM_BACKENDS = ['none', 'anthropic', 'vertex', 'gateway', 'claude-code'] as const;
+const KTX_LLM_BACKENDS = ['none', 'anthropic', 'vertex', 'gateway', 'claude-code', 'codex'] as const;
const KTX_EMBEDDING_BACKENDS = ['none', 'openai', 'sentence-transformers'] as const;
const KTX_PROMPT_CACHE_TTLS = ['5m', '1h'] as const;
const KTX_ENRICHMENT_MODES = ['none', 'deterministic', 'llm'] as const;
@@ -38,7 +38,7 @@ const llmProviderSchema = z
.enum(KTX_LLM_BACKENDS)
.default('none')
.describe(
- 'LLM provider backend. "none" disables LLM features; "anthropic" / "vertex" / "gateway" require the matching nested credentials block; "claude-code" uses the local Claude Code session.',
+ 'LLM provider backend. "none" disables LLM features; "anthropic" / "vertex" / "gateway" require the matching nested credentials block; "claude-code" uses the local Claude Code session; "codex" uses the local Codex session.',
),
vertex: vertexProviderSchema.optional().describe('Vertex AI credentials, used when backend is "vertex".'),
anthropic: apiCredentialsSchema.optional().describe('Anthropic API credentials, used when backend is "anthropic".'),
diff --git a/packages/cli/src/llm/types.ts b/packages/cli/src/llm/types.ts
index 3f7f67e2..a190b1c0 100644
--- a/packages/cli/src/llm/types.ts
+++ b/packages/cli/src/llm/types.ts
@@ -3,7 +3,7 @@ import type { LanguageModel, TelemetrySettings, ToolCallRepairFunction, ToolSet
export const KTX_MODEL_ROLES = ['default', 'triage', 'candidateExtraction', 'curator', 'reconcile', 'repair'] as const;
export type KtxModelRole = (typeof KTX_MODEL_ROLES)[number];
-type KtxLlmBackend = 'anthropic' | 'vertex' | 'gateway' | 'claude-code';
+type KtxLlmBackend = 'anthropic' | 'vertex' | 'gateway' | 'claude-code' | 'codex';
export type KtxPromptCacheTtl = '5m' | '1h';
type KtxJsonValue =
diff --git a/packages/cli/src/setup-models.ts b/packages/cli/src/setup-models.ts
index 041eef5c..8e8cf30b 100644
--- a/packages/cli/src/setup-models.ts
+++ b/packages/cli/src/setup-models.ts
@@ -3,6 +3,9 @@ import { writeFile } from 'node:fs/promises';
import { promisify } from 'node:util';
import { resolveLocalKtxLlmConfig } from './context/llm/local-config.js';
import { runClaudeCodeAuthProbe } from './context/llm/claude-code-runtime.js';
+import { formatCodexIsolationWarning } from './context/llm/codex-isolation.js';
+import { runCodexAuthProbe } from './context/llm/codex-runtime.js';
+import { DEFAULT_CODEX_MODEL } from './context/llm/codex-models.js';
import { resolveKtxConfigReference } from './context/core/config-reference.js';
import { type KtxProjectConfig, type KtxProjectLlmConfig, serializeKtxProjectConfig } from './context/project/config.js';
import { loadKtxProject } from './context/project/project.js';
@@ -56,7 +59,7 @@ export interface AnthropicModelChoice {
recommended: boolean;
}
-export type KtxSetupLlmBackend = 'anthropic' | 'vertex' | 'claude-code';
+export type KtxSetupLlmBackend = 'anthropic' | 'vertex' | 'claude-code' | 'codex';
/** @internal */
export interface KtxSetupModelPromptAdapter {
@@ -82,6 +85,7 @@ export interface KtxSetupModelDeps {
model: string;
env?: NodeJS.ProcessEnv;
}) => Promise<{ ok: true } | { ok: false; message: string }>;
+ codexAuthProbe?: (input: { projectDir: string; model: string }) => Promise<{ ok: true } | { ok: false; message: string }>;
readGcloudProject?: () => Promise;
listGcloudProjects?: () => Promise;
spinner?: () => KtxCliSpinner;
@@ -110,6 +114,20 @@ const CLAUDE_CODE_MODELS: AnthropicModelChoice[] = [
{ id: 'haiku', label: 'Claude Haiku', recommended: false },
];
+// Curated Codex models from OpenAI's current lineup that work under both
+// ChatGPT-account (subscription) and API-key auth. Intentionally omitted:
+// the `*-codex` ids (e.g. gpt-5.3-codex, gpt-5.2-codex) are API-key-only and
+// fail on ChatGPT-account auth, and gpt-5.3-codex-spark is a ChatGPT-Pro-only
+// research preview. Codex resolves real availability per account at runtime
+// (its binary remote-fetches the model list), so this is a convenience
+// shortlist only — the manual-entry option accepts any id your account's
+// `codex` picker exposes, and the auth probe reports an unsupported choice.
+const CODEX_MODELS: AnthropicModelChoice[] = [
+ { id: 'gpt-5.5', label: 'GPT-5.5', recommended: true },
+ { id: 'gpt-5.4', label: 'GPT-5.4', recommended: false },
+ { id: 'gpt-5.4-mini', label: 'GPT-5.4 mini', recommended: false },
+];
+
const HIDDEN_ANTHROPIC_MODEL_PATTERNS = [
/^claude-sonnet-4$/i,
/^claude-opus-4$/i,
@@ -272,7 +290,12 @@ export function isKtxSetupLlmConfigReady(config: KtxProjectLlmConfig): boolean {
return typeof resolved.vertex?.location === 'string' && resolved.vertex.location.trim().length > 0;
}
- return resolved.backend === 'anthropic' || resolved.backend === 'gateway' || resolved.backend === 'claude-code';
+ return (
+ resolved.backend === 'anthropic' ||
+ resolved.backend === 'gateway' ||
+ resolved.backend === 'claude-code' ||
+ resolved.backend === 'codex'
+ );
}
function hasUsableConfiguredLlm(config: KtxProjectConfig): boolean {
@@ -284,7 +307,8 @@ function buildProjectLlmConfig(
provider:
| { backend: 'anthropic'; credentialRef: string }
| { backend: 'vertex'; vertex: { project?: string; location: string } }
- | { backend: 'claude-code' },
+ | { backend: 'claude-code' }
+ | { backend: 'codex' },
model: string,
): KtxProjectLlmConfig {
if (provider.backend === 'claude-code') {
@@ -295,6 +319,14 @@ function buildProjectLlmConfig(
};
}
+ if (provider.backend === 'codex') {
+ return {
+ provider: { backend: 'codex' },
+ models: { ...existing.models, default: model },
+ promptCaching: existing.promptCaching,
+ };
+ }
+
if (provider.backend === 'vertex') {
return {
provider: {
@@ -515,6 +547,7 @@ async function chooseBackend(
message: 'Which LLM provider should KTX use?',
options: [
{ value: 'claude-code', label: 'Claude subscription (Pro/Max)' },
+ { value: 'codex', label: 'Codex subscription' },
{ value: 'anthropic', label: 'Anthropic API key' },
{ value: 'vertex', label: 'Google Vertex AI for Anthropic Claude' },
{ value: 'back', label: 'Back' },
@@ -525,7 +558,7 @@ async function chooseBackend(
}
return {
status: 'ready',
- backend: choice === 'vertex' || choice === 'claude-code' ? choice : 'anthropic',
+ backend: choice === 'vertex' || choice === 'claude-code' || choice === 'codex' ? choice : 'anthropic',
prompted: true,
};
}
@@ -884,12 +917,51 @@ async function chooseClaudeCodeModel(args: KtxSetupModelArgs, deps: KtxSetupMode
return { status: 'ready', model: choice };
}
+async function chooseCodexModel(args: KtxSetupModelArgs, deps: KtxSetupModelDeps): Promise {
+ const providedModel = requestedModel(args);
+ if (providedModel) {
+ return { status: 'ready', model: providedModel };
+ }
+ if (args.inputMode === 'disabled') {
+ return { status: 'ready', model: DEFAULT_CODEX_MODEL };
+ }
+
+ const prompts = deps.prompts ?? createPromptAdapter();
+ const choice = await prompts.select({
+ message: `Which Codex model should KTX use?\n\n${ANTHROPIC_MODEL_PROMPT_CONTEXT}`,
+ options: [
+ ...CODEX_MODELS.map((model) => ({
+ value: model.id,
+ label: model.label,
+ ...(model.recommended ? { hint: 'recommended' } : {}),
+ })),
+ { value: 'manual', label: 'Enter a Codex model ID manually' },
+ { value: 'back', label: 'Back' },
+ ],
+ });
+ if (choice === 'back') {
+ return { status: 'back' };
+ }
+ if (choice === 'manual') {
+ const manual = await prompts.text({
+ message: withTextInputNavigation('Codex model ID'),
+ placeholder: CODEX_MODELS.find((model) => model.recommended)?.id ?? CODEX_MODELS[0]?.id,
+ });
+ if (manual === undefined) {
+ return { status: 'back' };
+ }
+ return manual.trim() ? { status: 'ready', model: manual.trim() } : { status: 'missing-input' };
+ }
+ return { status: 'ready', model: choice };
+}
+
async function persistLlmConfig(
projectDir: string,
provider:
| { backend: 'anthropic'; credentialRef: string }
| { backend: 'vertex'; vertex: { project?: string; location: string } }
- | { backend: 'claude-code' },
+ | { backend: 'claude-code' }
+ | { backend: 'codex' },
model: string,
): Promise {
const project = await loadKtxProject({ projectDir });
@@ -1031,6 +1103,32 @@ export async function runKtxSetupAnthropicModelStep(
return { status: 'ready', projectDir: args.projectDir };
}
+ if (backendChoice.backend === 'codex') {
+ const model = await chooseCodexModel(backendArgs, deps);
+ if (model.status === 'back' && backendChoice.prompted) {
+ attemptArgs = buildInteractiveRetryArgs(args);
+ continue;
+ }
+ if (model.status === 'invalid-credential') {
+ return { status: 'failed', projectDir: args.projectDir };
+ }
+ if (model.status !== 'ready') {
+ return { status: model.status, projectDir: args.projectDir };
+ }
+ const probe = deps.codexAuthProbe ?? runCodexAuthProbe;
+ const health = await probe({ projectDir: args.projectDir, model: model.model });
+ if (!health.ok) {
+ io.stderr.write(`${health.message}\n`);
+ return { status: 'failed', projectDir: args.projectDir };
+ }
+ // Prefix the clack gutter so the warning sits inside the setup frame
+ // instead of breaking out of it; kept on stderr for scripted runs.
+ io.stderr.write(`│ ${formatCodexIsolationWarning()}\n`);
+ await persistLlmConfig(args.projectDir, { backend: 'codex' }, model.model);
+ io.stdout.write(`│ LLM ready: yes (codex, ${model.model})\n`);
+ return { status: 'ready', projectDir: args.projectDir };
+ }
+
const credential = await chooseCredentialRef(backendArgs, io, deps);
if (credential.status === 'back' && backendChoice.prompted) {
attemptArgs = buildInteractiveRetryArgs(args);
diff --git a/packages/cli/src/status-project.ts b/packages/cli/src/status-project.ts
index 097f4091..ff7b98f4 100644
--- a/packages/cli/src/status-project.ts
+++ b/packages/cli/src/status-project.ts
@@ -1,6 +1,11 @@
import { stat as statAsync, readdir as readdirAsync } from 'node:fs/promises';
import { basename, join } from 'node:path';
import { runClaudeCodeAuthProbe } from './context/llm/claude-code-runtime.js';
+import {
+ CODEX_ISOLATION_WARNING,
+ CODEX_ISOLATION_WARNING_FIX,
+} from './context/llm/codex-isolation.js';
+import { runCodexAuthProbe } from './context/llm/codex-runtime.js';
import type { KtxConfigIssue, KtxProjectConfig, KtxProjectConnectionConfig, KtxProjectEmbeddingConfig, KtxProjectLlmConfig } from './context/project/config.js';
import type { KtxLocalProject } from './context/project/project.js';
import { ktxLocalStateDbPath } from './context/project/local-state-db.js';
@@ -94,6 +99,11 @@ type ClaudeCodeAuthProbe = (input: {
env?: NodeJS.ProcessEnv;
}) => Promise<{ ok: true } | { ok: false; message: string }>;
+type CodexAuthProbe = (input: {
+ projectDir: string;
+ model: string;
+}) => Promise<{ ok: true } | { ok: false; message: string; fix: string }>;
+
const PROJECT_READY_COMMANDS = KTX_NEXT_STEP_DIRECT_COMMANDS.map((step) => step.command);
interface LocalStatsIngestPerConnection {
@@ -194,6 +204,7 @@ async function buildLlmStatus(
projectDir: string;
env: NodeJS.ProcessEnv;
claudeCodeAuthProbe?: ClaudeCodeAuthProbe;
+ codexAuthProbe?: CodexAuthProbe;
fast?: boolean;
useSpinner?: boolean;
},
@@ -210,6 +221,18 @@ async function buildLlmStatus(
fix: 'Run: ktx setup (choose an LLM provider)',
};
}
+ // The runtime (resolveModelSlots) hard-requires llm.models.default for every
+ // non-none backend; without it ingest/scan/memory throw. Report that here so
+ // status never marks a project ready that the runtime would refuse to run.
+ if (!model || model.trim().length === 0) {
+ return {
+ backend,
+ model,
+ status: 'fail',
+ detail: `llm.models.default is required for backend "${backend}"`,
+ fix: 'Set llm.models.default in ktx.yaml, then rerun `ktx status` (or rerun `ktx setup`).',
+ };
+ }
if (backend === 'anthropic') {
const ref = config.provider.anthropic?.api_key;
const resolved = resolveRef(ref, env);
@@ -251,7 +274,7 @@ async function buildLlmStatus(
};
}
if (backend === 'claude-code') {
- const modelName = model ?? 'sonnet';
+ const modelName = model;
if (options.fast === true) {
return {
backend,
@@ -280,6 +303,36 @@ async function buildLlmStatus(
fix: 'Authenticate Claude Code locally with the Claude Code CLI, then rerun `ktx status`.',
};
}
+ if (backend === 'codex') {
+ const modelName = model;
+ if (options.fast === true) {
+ return {
+ backend,
+ model: modelName,
+ status: 'skipped',
+ detail: 'auth probe skipped (--fast)',
+ };
+ }
+ const probe = options.codexAuthProbe ?? runCodexAuthProbe;
+ const auth = await withSpinner(options.useSpinner === true, 'Probing Codex authentication', () =>
+ probe({ projectDir: options.projectDir, model: modelName }),
+ );
+ if (auth.ok) {
+ return {
+ backend,
+ model: modelName,
+ status: 'ok',
+ detail: 'local Codex session authenticated',
+ };
+ }
+ return {
+ backend,
+ model: modelName,
+ status: 'fail',
+ detail: auth.message,
+ fix: auth.fix,
+ };
+ }
return { backend, model, status: 'warn', detail: 'unknown LLM backend' };
}
@@ -572,6 +625,13 @@ function buildWarnings(
});
}
+ if (llm.backend === 'codex') {
+ warnings.push({
+ message: CODEX_ISOLATION_WARNING,
+ fix: CODEX_ISOLATION_WARNING_FIX,
+ });
+ }
+
return warnings;
}
@@ -634,6 +694,7 @@ export interface BuildProjectStatusOptions {
env?: NodeJS.ProcessEnv;
queryHistoryReadinessProbe?: HistoricSqlReadinessProbe;
claudeCodeAuthProbe?: ClaudeCodeAuthProbe;
+ codexAuthProbe?: CodexAuthProbe;
configIssues?: KtxConfigIssue[];
fast?: boolean;
useSpinner?: boolean;
@@ -882,6 +943,7 @@ export async function buildProjectStatus(project: KtxLocalProject, options: Buil
projectDir: project.projectDir,
env,
claudeCodeAuthProbe: options.claudeCodeAuthProbe,
+ codexAuthProbe: options.codexAuthProbe,
fast: options.fast,
useSpinner: options.useSpinner,
});
diff --git a/packages/cli/test/context/ingest/local-bundle-runtime.test.ts b/packages/cli/test/context/ingest/local-bundle-runtime.test.ts
index 64fad53a..9d1ec9b4 100644
--- a/packages/cli/test/context/ingest/local-bundle-runtime.test.ts
+++ b/packages/cli/test/context/ingest/local-bundle-runtime.test.ts
@@ -77,9 +77,10 @@ describe('createLocalBundleIngestRuntime', () => {
}),
).toThrow(
[
- 'ktx ingest requires llm.provider.backend: anthropic, vertex, gateway, or claude-code, or an injected agentRunner.',
- 'Configure a local Claude Code session or API-backed LLM, then rerun ingest:',
+ 'ktx ingest requires llm.provider.backend: anthropic, vertex, gateway, claude-code, or codex, or an injected agentRunner.',
+ 'Configure a local Claude Code/Codex session or API-backed LLM, then rerun ingest:',
` ktx setup --project-dir ${project.projectDir} --llm-backend claude-code --no-input`,
+ ` ktx setup --project-dir ${project.projectDir} --llm-backend codex --llm-model gpt-5.5 --no-input`,
` ktx setup --project-dir ${project.projectDir} --llm-backend anthropic --anthropic-api-key-env ANTHROPIC_API_KEY --llm-model claude-sonnet-4-6 --no-input`,
].join('\n'),
);
diff --git a/packages/cli/test/context/llm/codex-exec-events.test.ts b/packages/cli/test/context/llm/codex-exec-events.test.ts
new file mode 100644
index 00000000..5edcfed8
--- /dev/null
+++ b/packages/cli/test/context/llm/codex-exec-events.test.ts
@@ -0,0 +1,188 @@
+import { describe, expect, it } from 'vitest';
+import {
+ parseCodexExecEventLine,
+ summarizeCodexExecEvents,
+} from '../../../src/context/llm/codex-exec-events.js';
+
+describe('Codex exec event parsing', () => {
+ it('uses the completed turn as one step when no MCP tools run', () => {
+ const summary = summarizeCodexExecEvents(
+ [
+ { type: 'thread.started', thread_id: 'thr_1' },
+ { type: 'turn.started' },
+ { type: 'item.completed', item: { id: 'item_1', type: 'agent_message', text: 'hello from codex' } },
+ {
+ type: 'turn.completed',
+ usage: {
+ input_tokens: 12,
+ cached_input_tokens: 4,
+ output_tokens: 5,
+ reasoning_output_tokens: 2,
+ },
+ },
+ ],
+ { startedAt: 100, now: () => 125 },
+ );
+
+ expect(summary).toEqual({
+ finalText: 'hello from codex',
+ stopReason: 'natural',
+ usage: { inputTokens: 12, outputTokens: 5, totalTokens: 17 },
+ stepCount: 1,
+ stepBoundariesMs: [25],
+ toolCallCount: 0,
+ toolFailures: [],
+ });
+ });
+
+ it('uses completed MCP tool calls as loop steps', () => {
+ const offsets = [115, 140, 175];
+ const summary = summarizeCodexExecEvents(
+ [
+ { type: 'turn.started' },
+ {
+ type: 'item.started',
+ item: { id: 'call_1', type: 'mcp_tool_call', server: 'ktx', tool: 'search', arguments: {}, status: 'in_progress' },
+ },
+ {
+ type: 'item.completed',
+ item: { id: 'call_1', type: 'mcp_tool_call', server: 'ktx', tool: 'search', arguments: {}, status: 'completed' },
+ },
+ {
+ type: 'item.started',
+ item: { id: 'call_2', type: 'mcp_tool_call', server: 'ktx', tool: 'lookup', arguments: {}, status: 'in_progress' },
+ },
+ {
+ type: 'item.completed',
+ item: {
+ id: 'call_2',
+ type: 'mcp_tool_call',
+ server: 'ktx',
+ tool: 'lookup',
+ arguments: {},
+ status: 'failed',
+ error: { message: 'denied' },
+ },
+ },
+ { type: 'item.completed', item: { id: 'item_1', type: 'agent_message', text: 'done' } },
+ { type: 'turn.completed', usage: { input_tokens: 1, output_tokens: 1, cached_input_tokens: 0, reasoning_output_tokens: 0 } },
+ ],
+ { startedAt: 100, now: () => offsets.shift() ?? 175 },
+ );
+
+ expect(summary).toEqual({
+ finalText: 'done',
+ stopReason: 'natural',
+ usage: { inputTokens: 1, outputTokens: 1, totalTokens: 2 },
+ stepCount: 2,
+ stepBoundariesMs: [15, 40],
+ toolCallCount: 2,
+ toolFailures: ['lookup: denied'],
+ });
+ });
+
+ it('does not treat a completed MCP tool call as failed when Codex sends error: null', () => {
+ // Captured verbatim from a real @openai/codex-sdk run: successful tool calls
+ // carry `error: null` and `result` alongside `status: "completed"`.
+ const summary = summarizeCodexExecEvents([
+ { type: 'turn.started' },
+ {
+ type: 'item.started',
+ item: {
+ id: 'item_1',
+ type: 'mcp_tool_call',
+ server: 'ktx',
+ tool: 'echo_value',
+ arguments: { value: 'ktx_codex_tool_ok' },
+ result: null,
+ error: null,
+ status: 'in_progress',
+ },
+ },
+ {
+ type: 'item.completed',
+ item: {
+ id: 'item_1',
+ type: 'mcp_tool_call',
+ server: 'ktx',
+ tool: 'echo_value',
+ arguments: { value: 'ktx_codex_tool_ok' },
+ result: { content: [{ type: 'text', text: 'echo:ktx_codex_tool_ok' }], structured_content: null },
+ error: null,
+ status: 'completed',
+ },
+ },
+ { type: 'item.completed', item: { id: 'm1', type: 'agent_message', text: 'done' } },
+ { type: 'turn.completed', usage: { input_tokens: 1, output_tokens: 1 } },
+ ]);
+
+ expect(summary.toolFailures).toEqual([]);
+ expect(summary.toolCallCount).toBe(1);
+ });
+
+ it('counts built-in command executions as loop steps without failing the loop', () => {
+ const offsets = [110, 130];
+ const summary = summarizeCodexExecEvents(
+ [
+ { type: 'turn.started' },
+ { type: 'item.completed', item: { id: 'c1', type: 'command_execution', command: 'ls', status: 'completed', exit_code: 0 } },
+ { type: 'item.completed', item: { id: 'c2', type: 'command_execution', command: 'cat missing', status: 'failed', exit_code: 1 } },
+ { type: 'item.completed', item: { id: 'm1', type: 'agent_message', text: 'done' } },
+ { type: 'turn.completed', usage: { input_tokens: 2, output_tokens: 1 } },
+ ],
+ { startedAt: 100, now: () => offsets.shift() ?? 130 },
+ );
+
+ expect(summary.stepCount).toBe(2);
+ expect(summary.stepBoundariesMs).toEqual([10, 30]);
+ // A non-zero command exit is normal agent exploration, not a runtime tool failure.
+ expect(summary.toolFailures).toEqual([]);
+ expect(summary.toolCallCount).toBe(0);
+ });
+
+ it('maps turn failures into error stop reason', () => {
+ const summary = summarizeCodexExecEvents([
+ { type: 'turn.started' },
+ { type: 'turn.failed', error: { message: 'Codex could not connect to required MCP server' } },
+ ]);
+
+ expect(summary.stopReason).toBe('error');
+ expect(summary.error?.message).toContain('Codex could not connect to required MCP server');
+ });
+
+ it('unwraps the Codex API error envelope into its human-readable message', () => {
+ // Codex serializes API errors as a JSON envelope inside the event message.
+ const apiError = JSON.stringify({
+ type: 'error',
+ status: 400,
+ error: {
+ type: 'invalid_request_error',
+ message: "The 'gpt-5.3-codex' model is not supported when using Codex with a ChatGPT account.",
+ },
+ });
+ const summary = summarizeCodexExecEvents([
+ { type: 'thread.started', thread_id: 'thr_1' },
+ { type: 'turn.started' },
+ { type: 'error', message: apiError },
+ { type: 'turn.failed', error: { message: apiError } },
+ ]);
+
+ expect(summary.stopReason).toBe('error');
+ expect(summary.error?.message).toBe(
+ "The 'gpt-5.3-codex' model is not supported when using Codex with a ChatGPT account.",
+ );
+ });
+
+ it('maps max-turns terminal reasons into budget stop reason when Codex emits one', () => {
+ const summary = summarizeCodexExecEvents([
+ { type: 'turn.started' },
+ { type: 'turn.completed', reason: 'max_turns', usage: { input_tokens: 1, output_tokens: 1 } },
+ ]);
+
+ expect(summary.stopReason).toBe('budget');
+ });
+
+ it('throws a clear error for malformed JSONL lines', () => {
+ expect(() => parseCodexExecEventLine('{not-json')).toThrow('Codex JSONL event stream was malformed');
+ });
+});
diff --git a/packages/cli/test/context/llm/codex-isolation.test.ts b/packages/cli/test/context/llm/codex-isolation.test.ts
new file mode 100644
index 00000000..0ef39ee3
--- /dev/null
+++ b/packages/cli/test/context/llm/codex-isolation.test.ts
@@ -0,0 +1,19 @@
+import { describe, expect, it } from 'vitest';
+import {
+ CODEX_ISOLATION_WARNING,
+ CODEX_ISOLATION_WARNING_FIX,
+ formatCodexIsolationWarning,
+} from '../../../src/context/llm/codex-isolation.js';
+
+describe('Codex isolation warning', () => {
+ it('documents the enforced and unenforced Codex isolation boundaries', () => {
+ expect(CODEX_ISOLATION_WARNING).toContain('runtime MCP server to the current ktx tool set');
+ expect(CODEX_ISOLATION_WARNING).toContain('disables Codex web search');
+ expect(CODEX_ISOLATION_WARNING).toContain('may still load user Codex config');
+ expect(CODEX_ISOLATION_WARNING).toContain('built-in command execution');
+ expect(CODEX_ISOLATION_WARNING_FIX).toContain('claude-code');
+ expect(formatCodexIsolationWarning()).toBe(
+ `${CODEX_ISOLATION_WARNING} ${CODEX_ISOLATION_WARNING_FIX}`,
+ );
+ });
+});
diff --git a/packages/cli/test/context/llm/codex-mcp-runtime-server.test.ts b/packages/cli/test/context/llm/codex-mcp-runtime-server.test.ts
new file mode 100644
index 00000000..c793afb7
--- /dev/null
+++ b/packages/cli/test/context/llm/codex-mcp-runtime-server.test.ts
@@ -0,0 +1,73 @@
+import { describe, expect, it, vi } from 'vitest';
+import { z } from 'zod';
+import {
+ createCodexRuntimeMcpServer,
+ startCodexRuntimeMcpServer,
+} from '../../../src/context/llm/codex-mcp-runtime-server.js';
+
+describe('Codex runtime MCP server', () => {
+ it('registers runtime tools with markdown output', async () => {
+ const registered = new Map<
+ string,
+ {
+ config: { description?: string; inputSchema: unknown };
+ handler: (input: Record) => Promise;
+ }
+ >();
+ const server = createCodexRuntimeMcpServer({
+ server: {
+ registerTool(name, config, handler) {
+ registered.set(name, { config, handler });
+ },
+ },
+ toolSet: {
+ wiki_search: {
+ name: 'wiki_search',
+ description: 'Search the wiki',
+ inputSchema: z.object({ query: z.string() }),
+ execute: vi.fn(async () => ({ markdown: 'result markdown', structured: { matches: 1 } })),
+ },
+ },
+ });
+
+ expect(server).toBeDefined();
+ expect([...registered.keys()]).toEqual(['wiki_search']);
+ expect(registered.get('wiki_search')?.config).toMatchObject({
+ description: 'Search the wiki',
+ });
+ await expect(registered.get('wiki_search')?.handler({ query: 'revenue' })).resolves.toEqual({
+ content: [{ type: 'text', text: 'result markdown' }],
+ structuredContent: { matches: 1 },
+ });
+ });
+
+ it('starts loopback HTTP MCP with a bearer token and reports the runtime URL', async () => {
+ const close = vi.fn(async () => undefined);
+ const runServer = vi.fn(async () => ({
+ server: { address: () => ({ port: 4321 }) },
+ close,
+ }));
+
+ const handle = await startCodexRuntimeMcpServer({
+ projectDir: '/tmp/ktx-project',
+ toolSet: {},
+ runServer: runServer as never,
+ });
+
+ expect(handle.url).toBe('http://127.0.0.1:4321/mcp');
+ expect(handle.bearerTokenEnvVar).toBe('KTX_CODEX_RUNTIME_MCP_TOKEN');
+ expect(handle.bearerToken).toMatch(/^[a-f0-9]{64}$/);
+ expect(runServer).toHaveBeenCalledWith(
+ expect.objectContaining({
+ projectDir: '/tmp/ktx-project',
+ host: '127.0.0.1',
+ port: 0,
+ token: handle.bearerToken,
+ allowedHosts: ['127.0.0.1', 'localhost'],
+ allowedOrigins: [],
+ }),
+ );
+ await handle.close();
+ expect(close).toHaveBeenCalled();
+ });
+});
diff --git a/packages/cli/test/context/llm/codex-models.test.ts b/packages/cli/test/context/llm/codex-models.test.ts
new file mode 100644
index 00000000..83a1e2c8
--- /dev/null
+++ b/packages/cli/test/context/llm/codex-models.test.ts
@@ -0,0 +1,17 @@
+import { describe, expect, it } from 'vitest';
+import { resolveCodexModel } from '../../../src/context/llm/codex-models.js';
+
+describe('resolveCodexModel', () => {
+ it.each([
+ ['codex', 'gpt-5.5'],
+ ['default', 'gpt-5.5'],
+ ['gpt-5.3-codex-spark', 'gpt-5.3-codex-spark'],
+ ['gpt-5.4', 'gpt-5.4'],
+ ])('maps %s to %s', (input, expected) => {
+ expect(resolveCodexModel(input)).toBe(expected);
+ });
+
+ it.each(['', ' ', 'sonnet', 'claude-sonnet-4-6'])('rejects %s', (input) => {
+ expect(() => resolveCodexModel(input)).toThrow('Unsupported Codex model');
+ });
+});
diff --git a/packages/cli/test/context/llm/codex-runtime-config.test.ts b/packages/cli/test/context/llm/codex-runtime-config.test.ts
new file mode 100644
index 00000000..97c80446
--- /dev/null
+++ b/packages/cli/test/context/llm/codex-runtime-config.test.ts
@@ -0,0 +1,43 @@
+import { describe, expect, it } from 'vitest';
+import { buildCodexRuntimeConfig } from '../../../src/context/llm/codex-runtime-config.js';
+
+describe('buildCodexRuntimeConfig', () => {
+ it('builds generic config without SDK thread-option fields', () => {
+ expect(buildCodexRuntimeConfig({ model: 'gpt-5.3-codex' })).toEqual({
+ configOverrides: {
+ history: { persistence: 'none' },
+ },
+ env: {},
+ });
+ });
+
+ it('adds only the temporary ktx MCP server and exact enabled tools', () => {
+ expect(
+ buildCodexRuntimeConfig({
+ model: 'gpt-5.3-codex',
+ mcp: {
+ url: 'http://127.0.0.1:4567/mcp',
+ bearerTokenEnvVar: 'KTX_CODEX_RUNTIME_MCP_TOKEN',
+ bearerToken: 'secret-token',
+ toolNames: ['sl_read_source', 'wiki_search'],
+ },
+ }),
+ ).toEqual({
+ configOverrides: {
+ history: { persistence: 'none' },
+ mcp_servers: {
+ ktx: {
+ url: 'http://127.0.0.1:4567/mcp',
+ bearer_token_env_var: 'KTX_CODEX_RUNTIME_MCP_TOKEN',
+ enabled_tools: ['sl_read_source', 'wiki_search'],
+ default_tools_approval_mode: 'approve',
+ required: true,
+ },
+ },
+ },
+ env: {
+ KTX_CODEX_RUNTIME_MCP_TOKEN: 'secret-token',
+ },
+ });
+ });
+});
diff --git a/packages/cli/test/context/llm/codex-runtime.test.ts b/packages/cli/test/context/llm/codex-runtime.test.ts
new file mode 100644
index 00000000..2d408543
--- /dev/null
+++ b/packages/cli/test/context/llm/codex-runtime.test.ts
@@ -0,0 +1,460 @@
+import { describe, expect, it, vi } from 'vitest';
+import { z } from 'zod';
+import {
+ CodexKtxLlmRuntime,
+ runCodexAuthProbe,
+} from '../../../src/context/llm/codex-runtime.js';
+
+async function* events(items: unknown[]) {
+ for (const item of items) {
+ yield item;
+ }
+}
+
+function runner(items: unknown[]) {
+ return {
+ runStreamed: vi.fn(async () => events(items)),
+ };
+}
+
+/** Yields the given events, then throws — mirroring the SDK throwing on a non-zero codex exec exit. */
+function throwingRunner(items: unknown[], error: Error) {
+ return {
+ runStreamed: vi.fn(async () =>
+ (async function* () {
+ for (const item of items) {
+ yield item;
+ }
+ throw error;
+ })(),
+ ),
+ };
+}
+
+const MODEL_UNSUPPORTED_API_ERROR = JSON.stringify({
+ type: 'error',
+ status: 400,
+ error: {
+ type: 'invalid_request_error',
+ message: "The 'gpt-5.3-codex' model is not supported when using Codex with a ChatGPT account.",
+ },
+});
+
+function budgetRunner() {
+ let observedSignal: AbortSignal | undefined;
+ return {
+ observedSignal: () => observedSignal,
+ runStreamed: vi.fn(async (input: { signal?: AbortSignal }) => {
+ observedSignal = input.signal;
+ return events([
+ { type: 'turn.started' },
+ { type: 'item.started', item: { type: 'mcp_tool_call', server: 'ktx', tool: 'first', status: 'in_progress' } },
+ { type: 'item.completed', item: { type: 'mcp_tool_call', server: 'ktx', tool: 'first', status: 'completed' } },
+ { type: 'item.started', item: { type: 'mcp_tool_call', server: 'ktx', tool: 'second', status: 'in_progress' } },
+ { type: 'item.completed', item: { type: 'mcp_tool_call', server: 'ktx', tool: 'second', status: 'completed' } },
+ { type: 'turn.completed', usage: { input_tokens: 1, output_tokens: 1 } },
+ ]);
+ }),
+ };
+}
+
+describe('CodexKtxLlmRuntime', () => {
+ it('generates text with the role-selected model and metrics', async () => {
+ const onMetrics = vi.fn();
+ const fakeRunner = runner([
+ { type: 'turn.started' },
+ { type: 'item.completed', item: { type: 'agent_message', text: 'hello' } },
+ { type: 'turn.completed', usage: { input_tokens: 3, output_tokens: 4, total_tokens: 7 } },
+ ]);
+ const runtime = new CodexKtxLlmRuntime({
+ projectDir: '/tmp/project',
+ modelSlots: { default: 'codex', triage: 'gpt-5.4' },
+ runner: fakeRunner,
+ });
+
+ await expect(runtime.generateText({ role: 'triage', system: 'system', prompt: 'prompt', onMetrics })).resolves.toBe('hello');
+ expect(fakeRunner.runStreamed).toHaveBeenCalledWith(
+ expect.objectContaining({
+ projectDir: '/tmp/project',
+ model: 'gpt-5.4',
+ prompt: 'system\n\nprompt',
+ }),
+ );
+ expect(onMetrics).toHaveBeenCalledWith(expect.objectContaining({ usage: { inputTokens: 3, outputTokens: 4, totalTokens: 7 } }));
+ });
+
+ it('generates and validates structured output', async () => {
+ const fakeRunner = runner([
+ { type: 'turn.started' },
+ { type: 'item.completed', item: { type: 'agent_message', text: '{"answer":"yes"}' } },
+ { type: 'turn.completed' },
+ ]);
+ const runtime = new CodexKtxLlmRuntime({
+ projectDir: '/tmp/project',
+ modelSlots: { default: 'codex' },
+ runner: fakeRunner,
+ });
+
+ await expect(
+ runtime.generateObject({
+ role: 'default',
+ prompt: 'json',
+ schema: z.object({ answer: z.string() }),
+ }),
+ ).resolves.toEqual({ answer: 'yes' });
+ expect(fakeRunner.runStreamed).toHaveBeenCalledWith(
+ expect.objectContaining({
+ outputSchema: expect.objectContaining({ type: 'object' }),
+ }),
+ );
+ });
+
+ it('returns a structured-output error when Codex final text is invalid JSON', async () => {
+ const fakeRunner = runner([
+ { type: 'turn.started' },
+ { type: 'item.completed', item: { type: 'agent_message', text: 'not json' } },
+ { type: 'turn.completed' },
+ ]);
+ const runtime = new CodexKtxLlmRuntime({
+ projectDir: '/tmp/project',
+ modelSlots: { default: 'codex' },
+ runner: fakeRunner,
+ });
+
+ await expect(
+ runtime.generateObject({
+ role: 'default',
+ prompt: 'json',
+ schema: z.object({ answer: z.string() }),
+ }),
+ ).rejects.toThrow('Codex structured output failed validation');
+ });
+
+ it('starts and closes a temporary MCP server for tool-backed agent loops', async () => {
+ const close = vi.fn(async () => undefined);
+ const startMcpServer = vi.fn(async () => ({
+ url: 'http://127.0.0.1:4321/mcp',
+ bearerTokenEnvVar: 'KTX_CODEX_RUNTIME_MCP_TOKEN' as const,
+ bearerToken: 'token',
+ close,
+ }));
+ const fakeRunner = runner([
+ { type: 'turn.started' },
+ { type: 'item.started', item: { type: 'mcp_tool_call', name: 'wiki_search' } },
+ { type: 'item.completed', item: { type: 'agent_message', text: 'done' } },
+ { type: 'turn.completed', usage: { input_tokens: 1, output_tokens: 1, total_tokens: 2 } },
+ ]);
+ const runtime = new CodexKtxLlmRuntime({
+ projectDir: '/tmp/project',
+ modelSlots: { default: 'codex' },
+ runner: fakeRunner,
+ startMcpServer,
+ });
+ const onStepFinish = vi.fn();
+
+ const result = await runtime.runAgentLoop({
+ modelRole: 'default',
+ systemPrompt: 'system',
+ userPrompt: 'user',
+ stepBudget: 5,
+ telemetryTags: {},
+ onStepFinish,
+ toolSet: {
+ aliased_wiki_tool: {
+ name: 'wiki_search',
+ description: 'Search wiki',
+ inputSchema: z.object({ query: z.string() }),
+ execute: vi.fn(),
+ },
+ },
+ });
+
+ expect(result.stopReason).toBe('natural');
+ expect(result.metrics).toMatchObject({ stepCount: 1, usage: { inputTokens: 1, outputTokens: 1, totalTokens: 2 } });
+ expect(onStepFinish).toHaveBeenCalledWith({ stepIndex: 1, stepBudget: 5 });
+ expect(startMcpServer).toHaveBeenCalledWith({ projectDir: '/tmp/project', toolSet: expect.any(Object) });
+ expect(fakeRunner.runStreamed).toHaveBeenCalledWith(
+ expect.objectContaining({
+ env: { KTX_CODEX_RUNTIME_MCP_TOKEN: 'token' },
+ configOverrides: expect.objectContaining({
+ mcp_servers: expect.objectContaining({
+ ktx: expect.objectContaining({
+ url: 'http://127.0.0.1:4321/mcp',
+ enabled_tools: ['wiki_search'],
+ required: true,
+ }),
+ }),
+ }),
+ }),
+ );
+ expect(close).toHaveBeenCalled();
+ });
+
+ it('returns error stop reason on turn failure', async () => {
+ const runtime = new CodexKtxLlmRuntime({
+ projectDir: '/tmp/project',
+ modelSlots: { default: 'codex' },
+ runner: runner([{ type: 'turn.failed', error: { message: 'boom' } }]),
+ });
+
+ const result = await runtime.runAgentLoop({
+ modelRole: 'default',
+ systemPrompt: 'system',
+ userPrompt: 'user',
+ stepBudget: 5,
+ telemetryTags: {},
+ toolSet: {},
+ });
+
+ expect(result.stopReason).toBe('error');
+ expect(result.error?.message).toBe('boom');
+ });
+
+ it('surfaces failed MCP tool calls as agent-loop errors', async () => {
+ const runtime = new CodexKtxLlmRuntime({
+ projectDir: '/tmp/project',
+ modelSlots: { default: 'codex' },
+ runner: runner([
+ { type: 'turn.started' },
+ { type: 'item.started', item: { type: 'mcp_tool_call', server: 'ktx', tool: 'search', status: 'in_progress' } },
+ {
+ type: 'item.completed',
+ item: {
+ type: 'mcp_tool_call',
+ server: 'ktx',
+ tool: 'search',
+ status: 'failed',
+ error: { message: 'denied' },
+ },
+ },
+ { type: 'turn.completed', usage: { input_tokens: 1, output_tokens: 1 } },
+ ]),
+ });
+
+ const result = await runtime.runAgentLoop({
+ modelRole: 'default',
+ systemPrompt: 'system',
+ userPrompt: 'user',
+ stepBudget: 5,
+ telemetryTags: {},
+ toolSet: {},
+ });
+
+ expect(result.stopReason).toBe('error');
+ expect(result.error?.message).toBe('Codex runtime tool call failed: search: denied');
+ expect(result.metrics).toMatchObject({
+ stepCount: 1,
+ usage: { inputTokens: 1, outputTokens: 1, totalTokens: 2 },
+ });
+ });
+
+ it('returns budget and aborts the Codex stream when local MCP step budget is reached', async () => {
+ const fakeRunner = budgetRunner();
+ const runtime = new CodexKtxLlmRuntime({
+ projectDir: '/tmp/project',
+ modelSlots: { default: 'codex' },
+ runner: fakeRunner,
+ });
+ const onStepFinish = vi.fn();
+
+ const result = await runtime.runAgentLoop({
+ modelRole: 'default',
+ systemPrompt: 'system',
+ userPrompt: 'user',
+ stepBudget: 1,
+ telemetryTags: {},
+ onStepFinish,
+ toolSet: {
+ first: {
+ name: 'first',
+ description: 'First tool',
+ inputSchema: z.object({}),
+ execute: vi.fn(),
+ },
+ },
+ });
+
+ expect(result.stopReason).toBe('budget');
+ expect(result.error).toBeUndefined();
+ expect(result.metrics).toMatchObject({ stepCount: 1 });
+ expect(onStepFinish).toHaveBeenCalledTimes(1);
+ expect(onStepFinish).toHaveBeenCalledWith({ stepIndex: 1, stepBudget: 1 });
+ expect(fakeRunner.observedSignal()?.aborted).toBe(true);
+ });
+
+ it('counts built-in command_execution steps against the budget and aborts the stream', async () => {
+ let observedSignal: AbortSignal | undefined;
+ const fakeRunner = {
+ observedSignal: () => observedSignal,
+ runStreamed: vi.fn(async (input: { signal?: AbortSignal }) => {
+ observedSignal = input.signal;
+ return events([
+ { type: 'turn.started' },
+ { type: 'item.started', item: { type: 'command_execution', command: 'ls', status: 'in_progress' } },
+ { type: 'item.completed', item: { type: 'command_execution', command: 'ls', status: 'completed', exit_code: 0 } },
+ { type: 'item.started', item: { type: 'command_execution', command: 'cat a', status: 'in_progress' } },
+ { type: 'item.completed', item: { type: 'command_execution', command: 'cat a', status: 'completed', exit_code: 0 } },
+ { type: 'item.completed', item: { type: 'command_execution', command: 'cat b', status: 'completed', exit_code: 0 } },
+ { type: 'turn.completed', usage: { input_tokens: 1, output_tokens: 1 } },
+ ]);
+ }),
+ };
+ const runtime = new CodexKtxLlmRuntime({
+ projectDir: '/tmp/project',
+ modelSlots: { default: 'codex' },
+ runner: fakeRunner,
+ });
+ const onStepFinish = vi.fn();
+
+ const result = await runtime.runAgentLoop({
+ modelRole: 'default',
+ systemPrompt: 'system',
+ userPrompt: 'user',
+ stepBudget: 2,
+ telemetryTags: {},
+ onStepFinish,
+ toolSet: {},
+ });
+
+ expect(result.stopReason).toBe('budget');
+ expect(result.error).toBeUndefined();
+ expect(result.metrics).toMatchObject({ stepCount: 2 });
+ expect(onStepFinish).toHaveBeenCalledTimes(2);
+ expect(onStepFinish).toHaveBeenLastCalledWith({ stepIndex: 2, stepBudget: 2 });
+ expect(fakeRunner.observedSignal()?.aborted).toBe(true);
+ });
+
+ it('fires onStepFinish live as each step completes, before the stream drains', async () => {
+ const order: string[] = [];
+ async function* liveEvents() {
+ yield { type: 'turn.started' };
+ yield { type: 'item.completed', item: { type: 'mcp_tool_call', server: 'ktx', tool: 'a', status: 'completed' } };
+ order.push('yielded-after-step-1');
+ yield { type: 'item.completed', item: { type: 'mcp_tool_call', server: 'ktx', tool: 'b', status: 'completed' } };
+ order.push('yielded-after-step-2');
+ yield { type: 'item.completed', item: { type: 'agent_message', text: 'done' } };
+ yield { type: 'turn.completed', usage: { input_tokens: 1, output_tokens: 1 } };
+ }
+ const fakeRunner = { runStreamed: vi.fn(async () => liveEvents()) };
+ const runtime = new CodexKtxLlmRuntime({
+ projectDir: '/tmp/project',
+ modelSlots: { default: 'codex' },
+ runner: fakeRunner,
+ });
+
+ const result = await runtime.runAgentLoop({
+ modelRole: 'default',
+ systemPrompt: 'system',
+ userPrompt: 'user',
+ stepBudget: 10,
+ telemetryTags: {},
+ onStepFinish: ({ stepIndex }) => {
+ order.push(`step-${stepIndex}`);
+ },
+ toolSet: {},
+ });
+
+ expect(result.stopReason).toBe('natural');
+ expect(result.metrics).toMatchObject({ stepCount: 2 });
+ expect(order).toEqual(['step-1', 'yielded-after-step-1', 'step-2', 'yielded-after-step-2']);
+ });
+
+ it('surfaces the real Codex error event even when the SDK stream throws afterward', async () => {
+ // The SDK yields the error/turn.failed events on stdout, then throws on the
+ // non-zero exit. The masked exit message must not hide the real API error.
+ const fakeRunner = throwingRunner(
+ [
+ { type: 'thread.started', thread_id: 't' },
+ { type: 'turn.started' },
+ { type: 'error', message: MODEL_UNSUPPORTED_API_ERROR },
+ { type: 'turn.failed', error: { message: MODEL_UNSUPPORTED_API_ERROR } },
+ ],
+ new Error('Codex Exec exited with code 1: Reading prompt from stdin...'),
+ );
+ const runtime = new CodexKtxLlmRuntime({
+ projectDir: '/tmp/project',
+ modelSlots: { default: 'codex' },
+ runner: fakeRunner,
+ });
+
+ await expect(runtime.generateText({ role: 'default', prompt: 'hi' })).rejects.toThrow(
+ 'not supported when using Codex with a ChatGPT account',
+ );
+ });
+
+ it('probes Codex authentication through a minimal non-interactive turn', async () => {
+ const fakeRunner = runner([
+ { type: 'turn.started' },
+ { type: 'item.completed', item: { type: 'agent_message', text: 'ok' } },
+ { type: 'turn.completed' },
+ ]);
+
+ await expect(
+ runCodexAuthProbe({
+ projectDir: '/tmp/project',
+ model: 'codex',
+ runner: fakeRunner,
+ }),
+ ).resolves.toEqual({ ok: true });
+ });
+
+ it('reports an unavailable model without blaming auth when Codex rejects the model', async () => {
+ const fakeRunner = throwingRunner(
+ [
+ { type: 'turn.started' },
+ { type: 'turn.failed', error: { message: MODEL_UNSUPPORTED_API_ERROR } },
+ ],
+ new Error('Codex Exec exited with code 1: Reading prompt from stdin...'),
+ );
+
+ const result = await runCodexAuthProbe({
+ projectDir: '/tmp/project',
+ model: 'gpt-5.3-codex',
+ runner: fakeRunner,
+ });
+
+ expect(result.ok).toBe(false);
+ if (!result.ok) {
+ expect(result.message).not.toContain('authentication is not usable');
+ expect(result.message).toContain('not available');
+ expect(result.message).toContain('gpt-5.3-codex');
+ expect(result.message).toContain('not supported when using Codex with a ChatGPT account');
+ // A model-access failure must steer the user at the model config, not auth.
+ expect(result.fix).toContain('llm.models.default');
+ expect(result.fix).not.toContain('Authenticate Codex');
+ }
+ });
+
+ it('reports an auth failure when Codex exits without an error event', async () => {
+ const fakeRunner = throwingRunner(
+ [],
+ new Error('Codex Exec exited with code 1: Not logged in. Run `codex login`.'),
+ );
+
+ const result = await runCodexAuthProbe({
+ projectDir: '/tmp/project',
+ model: 'gpt-5.5',
+ runner: fakeRunner,
+ });
+
+ expect(result.ok).toBe(false);
+ if (!result.ok) {
+ expect(result.message).toContain('authentication is not usable');
+ expect(result.message).toContain('Not logged in');
+ expect(result.fix).toContain('Authenticate Codex');
+ }
+ });
+
+ it('rejects an unsupported model id before probing, steering at llm.models.default', async () => {
+ const result = await runCodexAuthProbe({
+ projectDir: '/tmp/project',
+ model: 'not-a-real-model',
+ });
+
+ expect(result.ok).toBe(false);
+ if (!result.ok) {
+ expect(result.message).toContain('Unsupported Codex model');
+ expect(result.fix).toContain('llm.models.default');
+ }
+ });
+});
diff --git a/packages/cli/test/context/llm/codex-sdk-runner.test.ts b/packages/cli/test/context/llm/codex-sdk-runner.test.ts
new file mode 100644
index 00000000..fdafc666
--- /dev/null
+++ b/packages/cli/test/context/llm/codex-sdk-runner.test.ts
@@ -0,0 +1,97 @@
+import { describe, expect, it, vi } from 'vitest';
+
+const sdkMock = vi.hoisted(() => {
+ const events = (async function* () {
+ yield { type: 'turn.completed', usage: { input_tokens: 1, output_tokens: 2 } };
+ })();
+ const runStreamed = vi.fn(async () => ({ events }));
+ const startThread = vi.fn(() => ({ runStreamed }));
+ const Codex = vi.fn(function Codex(this: { startThread: typeof startThread }, options?: unknown) {
+ Object.assign(this, { options, startThread });
+ });
+ return { Codex, startThread, runStreamed };
+});
+
+vi.mock('@openai/codex-sdk', () => ({ Codex: sdkMock.Codex }));
+
+import { CodexSdkCliRunner } from '../../../src/context/llm/codex-sdk-runner.js';
+
+async function collectAsync(items: AsyncIterable): Promise {
+ const collected: T[] = [];
+ for await (const item of items) {
+ collected.push(item);
+ }
+ return collected;
+}
+
+describe('CodexSdkCliRunner', () => {
+ it('passes isolated env through the SDK and runtime controls through thread options', async () => {
+ const runner = new CodexSdkCliRunner({
+ envBase: {
+ HOME: '/home/ktx-user',
+ PATH: '/usr/local/bin:/usr/bin',
+ CODEX_HOME: '/home/ktx-user/.codex',
+ HTTPS_PROXY: 'http://proxy.example',
+ KTX_UNRELATED_SECRET: 'must-not-copy', // pragma: allowlist secret
+ },
+ });
+ const previousToken = process.env.KTX_CODEX_RUNTIME_MCP_TOKEN;
+ process.env.KTX_CODEX_RUNTIME_MCP_TOKEN = 'outer-token';
+ const outputSchema = {
+ type: 'object',
+ properties: { answer: { type: 'string' } },
+ required: ['answer'],
+ additionalProperties: false,
+ };
+ const controller = new AbortController();
+
+ try {
+ const events = await runner.runStreamed({
+ projectDir: '/tmp/ktx-project',
+ model: 'gpt-5.3-codex',
+ prompt: 'Return JSON.',
+ configOverrides: {
+ history: { persistence: 'none' },
+ },
+ env: { KTX_CODEX_RUNTIME_MCP_TOKEN: 'run-token' },
+ outputSchema,
+ signal: controller.signal,
+ });
+
+ expect(sdkMock.Codex).toHaveBeenCalledWith({
+ config: {
+ history: { persistence: 'none' },
+ },
+ env: {
+ HOME: '/home/ktx-user',
+ PATH: '/usr/local/bin:/usr/bin',
+ CODEX_HOME: '/home/ktx-user/.codex',
+ HTTPS_PROXY: 'http://proxy.example',
+ KTX_CODEX_RUNTIME_MCP_TOKEN: 'run-token',
+ },
+ });
+ expect(process.env.KTX_CODEX_RUNTIME_MCP_TOKEN).toBe('outer-token');
+ expect(sdkMock.startThread).toHaveBeenCalledWith({
+ workingDirectory: '/tmp/ktx-project',
+ skipGitRepoCheck: true,
+ model: 'gpt-5.3-codex',
+ sandboxMode: 'read-only',
+ webSearchMode: 'disabled',
+ approvalPolicy: 'never',
+ });
+ expect(sdkMock.runStreamed).toHaveBeenCalledWith('Return JSON.', {
+ outputSchema,
+ signal: controller.signal,
+ });
+ await expect(collectAsync(events)).resolves.toEqual([
+ { type: 'turn.completed', usage: { input_tokens: 1, output_tokens: 2 } },
+ ]);
+ } finally {
+ if (previousToken === undefined) {
+ delete process.env.KTX_CODEX_RUNTIME_MCP_TOKEN;
+ } else {
+ process.env.KTX_CODEX_RUNTIME_MCP_TOKEN = previousToken;
+ }
+ }
+ });
+});
diff --git a/packages/cli/test/context/llm/runtime-local-config.test.ts b/packages/cli/test/context/llm/runtime-local-config.test.ts
index 9e432cec..14adca7c 100644
--- a/packages/cli/test/context/llm/runtime-local-config.test.ts
+++ b/packages/cli/test/context/llm/runtime-local-config.test.ts
@@ -22,4 +22,25 @@ describe('local KTX LLM runtime config', () => {
}),
).toBeNull();
});
+
+ it('creates a Codex runtime for codex backend without creating an AI SDK provider', () => {
+ const runtime = createLocalKtxLlmRuntimeFromConfig(
+ {
+ provider: { backend: 'codex' },
+ models: { default: 'codex', triage: 'gpt-5.4' },
+ },
+ { env: {}, projectDir: '/tmp/project', createCodexRuntime: vi.fn((deps) => ({ deps }) as never) },
+ );
+
+ expect(runtime).toMatchObject({ deps: expect.objectContaining({ projectDir: '/tmp/project' }) });
+ });
+
+ it('returns null from the AI SDK provider factory for codex backend', () => {
+ expect(
+ createLocalKtxLlmProviderFromConfig({
+ provider: { backend: 'codex' },
+ models: { default: 'codex' },
+ }),
+ ).toBeNull();
+ });
});
diff --git a/packages/cli/test/context/project/config.test.ts b/packages/cli/test/context/project/config.test.ts
index 670e1696..6027d454 100644
--- a/packages/cli/test/context/project/config.test.ts
+++ b/packages/cli/test/context/project/config.test.ts
@@ -231,6 +231,31 @@ llm:
});
});
+ it('parses Codex as a first-class LLM backend', () => {
+ const config = parseKtxProjectConfig(`
+llm:
+ provider:
+ backend: codex
+ models:
+ default: gpt-5.3-codex
+ triage: gpt-5.3-codex
+ candidateExtraction: gpt-5.3-codex
+ curator: gpt-5.3-codex
+ reconcile: gpt-5.3-codex
+ repair: gpt-5.3-codex
+`);
+
+ expect(config.llm.provider.backend).toBe('codex');
+ expect(config.llm.models).toEqual({
+ default: 'gpt-5.3-codex',
+ triage: 'gpt-5.3-codex',
+ candidateExtraction: 'gpt-5.3-codex',
+ curator: 'gpt-5.3-codex',
+ reconcile: 'gpt-5.3-codex',
+ repair: 'gpt-5.3-codex',
+ });
+ });
+
it('parses gateway LLM, OpenAI scan embeddings, and sentence-transformers ingest embeddings', () => {
const config = parseKtxProjectConfig(`
llm:
@@ -530,7 +555,7 @@ describe('generateKtxProjectConfigJsonSchema', () => {
const llm = (schema.properties as Record }>).llm;
const provider = llm?.properties?.provider as { properties?: Record };
const backend = provider?.properties?.backend as { enum?: readonly string[] };
- expect(backend?.enum).toEqual(['none', 'anthropic', 'vertex', 'gateway', 'claude-code']);
+ expect(backend?.enum).toEqual(['none', 'anthropic', 'vertex', 'gateway', 'claude-code', 'codex']);
const storage = (schema.properties as Record }>).storage;
const state = storage?.properties?.state as { enum?: readonly string[] };
diff --git a/packages/cli/test/doctor.test.ts b/packages/cli/test/doctor.test.ts
index e3871f28..242331e8 100644
--- a/packages/cli/test/doctor.test.ts
+++ b/packages/cli/test/doctor.test.ts
@@ -422,6 +422,8 @@ describe('runKtxDoctor', () => {
'llm:',
' provider:',
' backend: anthropic',
+ ' models:',
+ ' default: claude-sonnet-4-5',
'',
].join('\n'),
'utf-8',
@@ -543,6 +545,8 @@ describe('runKtxDoctor', () => {
'llm:',
' provider:',
' backend: anthropic',
+ ' models:',
+ ' default: claude-sonnet-4-5',
'ingest:',
' adapters:',
' - live-database',
@@ -652,6 +656,8 @@ describe('runKtxDoctor', () => {
'llm:',
' provider:',
' backend: anthropic',
+ ' models:',
+ ' default: claude-sonnet-4-5',
'',
].join('\n'),
'utf-8',
@@ -698,6 +704,8 @@ describe('runKtxDoctor', () => {
'llm:',
' provider:',
' backend: anthropic',
+ ' models:',
+ ' default: claude-sonnet-4-5',
'ingest:',
' adapters:',
' - live-database',
diff --git a/packages/cli/test/ingest.test.ts b/packages/cli/test/ingest.test.ts
index f5cd1ac5..4fc47d0c 100644
--- a/packages/cli/test/ingest.test.ts
+++ b/packages/cli/test/ingest.test.ts
@@ -337,10 +337,13 @@ describe('runKtxIngest', () => {
expect(runIo.stdout()).toBe('');
expect(runIo.stderr()).toContain(
- 'ktx ingest requires llm.provider.backend: anthropic, vertex, gateway, or claude-code, or an injected agentRunner.',
+ 'ktx ingest requires llm.provider.backend: anthropic, vertex, gateway, claude-code, or codex, or an injected agentRunner.',
);
- expect(runIo.stderr()).toContain('Configure a local Claude Code session or API-backed LLM, then rerun ingest:');
+ expect(runIo.stderr()).toContain('Configure a local Claude Code/Codex session or API-backed LLM, then rerun ingest:');
expect(runIo.stderr()).toContain(`ktx setup --project-dir ${projectDir} --llm-backend claude-code --no-input`);
+ expect(runIo.stderr()).toContain(
+ `ktx setup --project-dir ${projectDir} --llm-backend codex --llm-model gpt-5.5 --no-input`,
+ );
expect(runIo.stderr()).toContain(
`ktx setup --project-dir ${projectDir} --llm-backend anthropic --anthropic-api-key-env ANTHROPIC_API_KEY --llm-model claude-sonnet-4-6 --no-input`,
);
diff --git a/packages/cli/test/llm/model-provider.test.ts b/packages/cli/test/llm/model-provider.test.ts
index 0e3ef045..17d47c6a 100644
--- a/packages/cli/test/llm/model-provider.test.ts
+++ b/packages/cli/test/llm/model-provider.test.ts
@@ -312,4 +312,13 @@ describe('createKtxLlmProvider', () => {
}),
).toThrow('claude-code is not an AI SDK LanguageModel backend');
});
+
+ it('rejects codex as an AI SDK LanguageModel backend', () => {
+ expect(() =>
+ createKtxLlmProvider({
+ backend: 'codex',
+ modelSlots: { default: 'gpt-5.3-codex' },
+ }),
+ ).toThrow('codex is not an AI SDK LanguageModel backend');
+ });
});
diff --git a/packages/cli/test/setup-models.test.ts b/packages/cli/test/setup-models.test.ts
index f054beff..dedf03bd 100644
--- a/packages/cli/test/setup-models.test.ts
+++ b/packages/cli/test/setup-models.test.ts
@@ -66,6 +66,7 @@ function makePromptAdapter(options: {
nextProviderChoice === 'anthropic' ||
nextProviderChoice === 'vertex' ||
nextProviderChoice === 'claude-code' ||
+ nextProviderChoice === 'codex' ||
nextProviderChoice === 'back'
) {
return selectValues.shift() ?? nextProviderChoice;
@@ -183,6 +184,7 @@ describe('setup Anthropic model step', () => {
message: expect.stringContaining('Which LLM provider should KTX use?'),
options: [
{ value: 'claude-code', label: 'Claude subscription (Pro/Max)' },
+ { value: 'codex', label: 'Codex subscription' },
{ value: 'anthropic', label: 'Anthropic API key' },
{ value: 'vertex', label: 'Google Vertex AI for Anthropic Claude' },
{ value: 'back', label: 'Back' },
@@ -215,6 +217,85 @@ describe('setup Anthropic model step', () => {
expect(authProbe).toHaveBeenCalledWith(expect.objectContaining({ projectDir: tempDir, model: 'sonnet' }));
});
+ it('configures Codex backend and validates local auth', async () => {
+ const io = makeIo();
+ const codexAuthProbe = vi.fn(async () => ({ ok: true as const }));
+
+ const result = await runKtxSetupAnthropicModelStep(
+ {
+ projectDir: tempDir,
+ inputMode: 'disabled',
+ llmBackend: 'codex',
+ llmModel: 'gpt-5.5',
+ skipLlm: false,
+ },
+ io.io,
+ { codexAuthProbe },
+ );
+
+ expect(result.status).toBe('ready');
+ const config = parseKtxProjectConfig(await readFile(join(tempDir, 'ktx.yaml'), 'utf-8'));
+ expect(config.llm).toMatchObject({
+ provider: { backend: 'codex' },
+ models: { default: 'gpt-5.5' },
+ });
+ expect(codexAuthProbe).toHaveBeenCalledWith(expect.objectContaining({ projectDir: tempDir, model: 'gpt-5.5' }));
+ // The warning carries the clack gutter so it renders inside the setup frame.
+ expect(io.stderr()).toContain('│ Codex backend isolation is limited');
+ expect(io.stderr()).toContain('may still load user Codex config');
+ });
+
+ it('defaults the Codex model to gpt-5.5 when none is provided non-interactively', async () => {
+ const io = makeIo();
+ const codexAuthProbe = vi.fn(async () => ({ ok: true as const }));
+
+ const result = await runKtxSetupAnthropicModelStep(
+ {
+ projectDir: tempDir,
+ inputMode: 'disabled',
+ llmBackend: 'codex',
+ skipLlm: false,
+ },
+ io.io,
+ { codexAuthProbe },
+ );
+
+ expect(result.status).toBe('ready');
+ const config = parseKtxProjectConfig(await readFile(join(tempDir, 'ktx.yaml'), 'utf-8'));
+ expect(config.llm).toMatchObject({
+ provider: { backend: 'codex' },
+ models: { default: 'gpt-5.5' },
+ });
+ expect(codexAuthProbe).toHaveBeenCalledWith(expect.objectContaining({ projectDir: tempDir, model: 'gpt-5.5' }));
+ });
+
+ it('offers the curated Codex models during interactive setup', async () => {
+ const io = makeIo();
+ const prompts = makePromptAdapter({ selectValues: ['codex', 'gpt-5.5'] });
+ const codexAuthProbe = vi.fn(async () => ({ ok: true as const }));
+
+ const result = await runKtxSetupAnthropicModelStep(
+ { projectDir: tempDir, inputMode: 'auto', skipLlm: false },
+ io.io,
+ { prompts, codexAuthProbe },
+ );
+
+ expect(result.status).toBe('ready');
+ expect(prompts.select).toHaveBeenCalledWith(
+ expect.objectContaining({
+ message: expect.stringContaining('Which Codex model should KTX use?'),
+ options: [
+ { value: 'gpt-5.5', label: 'GPT-5.5', hint: 'recommended' },
+ { value: 'gpt-5.4', label: 'GPT-5.4' },
+ { value: 'gpt-5.4-mini', label: 'GPT-5.4 mini' },
+ { value: 'manual', label: 'Enter a Codex model ID manually' },
+ { value: 'back', label: 'Back' },
+ ],
+ }),
+ );
+ expect(codexAuthProbe).toHaveBeenCalledWith(expect.objectContaining({ model: 'gpt-5.5' }));
+ });
+
it('prompts for the Claude Code model during interactive setup', async () => {
const io = makeIo();
const prompts = makePromptAdapter({ selectValues: ['claude-code', 'opus'] });
diff --git a/packages/cli/test/status-project.test.ts b/packages/cli/test/status-project.test.ts
index 38d5aa6f..cd63cf19 100644
--- a/packages/cli/test/status-project.test.ts
+++ b/packages/cli/test/status-project.test.ts
@@ -44,6 +44,17 @@ function withClaudeCodeLlm(config: KtxProjectConfig): KtxProjectConfig {
};
}
+function withCodexLlm(config: KtxProjectConfig): KtxProjectConfig {
+ return {
+ ...config,
+ llm: {
+ ...config.llm,
+ provider: { backend: 'codex' },
+ models: { ...config.llm.models, default: 'gpt-5.5' },
+ },
+ };
+}
+
function baseProjectConfig(): KtxProjectConfig {
return withClaudeCodeLlm(buildDefaultKtxProjectConfig());
}
@@ -391,6 +402,126 @@ describe('buildProjectStatus --fast', () => {
});
});
+describe('buildProjectStatus codex', () => {
+ it('reports authenticated local Codex session', async () => {
+ const project = projectWithConfig(withCodexLlm(buildDefaultKtxProjectConfig()));
+ const status = await buildProjectStatus(project, {
+ codexAuthProbe: async () => ({ ok: true as const }),
+ });
+
+ expect(status.llm).toMatchObject({
+ backend: 'codex',
+ model: 'gpt-5.5',
+ status: 'ok',
+ detail: 'local Codex session authenticated',
+ });
+ expect(status.warnings).toEqual(
+ expect.arrayContaining([
+ expect.objectContaining({
+ message: expect.stringContaining('Codex backend isolation is limited'),
+ fix: expect.stringContaining('claude-code'),
+ }),
+ ]),
+ );
+ const rendered = renderProjectStatus(status, { verbose: false, useColor: false });
+ expect(rendered).toContain('Codex backend isolation is limited');
+ });
+
+ it('skips Codex auth probe with --fast', async () => {
+ let probeCalls = 0;
+ const project = projectWithConfig(withCodexLlm(buildDefaultKtxProjectConfig()));
+ const status = await buildProjectStatus(project, {
+ fast: true,
+ codexAuthProbe: async () => {
+ probeCalls += 1;
+ return { ok: true };
+ },
+ });
+
+ expect(probeCalls).toBe(0);
+ expect(status.llm.status).toBe('skipped');
+ expect(status.llm.detail).toMatch(/--fast/);
+ });
+
+ it('surfaces the probe fix for a model-access failure instead of an auth fix', async () => {
+ const project = projectWithConfig(withCodexLlm(buildDefaultKtxProjectConfig()));
+ const status = await buildProjectStatus(project, {
+ codexAuthProbe: async () => ({
+ ok: false,
+ message: 'Codex is authenticated, but the configured model "gpt-5.5" is not available...',
+ fix: 'Run `codex` to see the models your account supports, then set llm.models.default in ktx.yaml (or rerun `ktx setup`).',
+ }),
+ });
+
+ expect(status.llm.status).toBe('fail');
+ expect(status.llm.fix).toContain('llm.models.default');
+ expect(status.llm.fix).not.toContain('Authenticate Codex');
+ });
+});
+
+describe('buildProjectStatus llm models.default requirement', () => {
+ function withBackendNoModel(
+ backend: KtxProjectConfig['llm']['provider']['backend'],
+ ): KtxProjectConfig {
+ const config = buildDefaultKtxProjectConfig();
+ return {
+ ...config,
+ llm: { ...config.llm, provider: { backend }, models: {} },
+ };
+ }
+
+ it('fails codex without llm.models.default and never probes', async () => {
+ let probeCalls = 0;
+ const project = projectWithConfig(withBackendNoModel('codex'));
+ const status = await buildProjectStatus(project, {
+ codexAuthProbe: async () => {
+ probeCalls += 1;
+ return { ok: true };
+ },
+ });
+
+ expect(probeCalls).toBe(0);
+ expect(status.llm.status).toBe('fail');
+ expect(status.llm.detail).toContain('llm.models.default');
+ expect(status.verdict).toBe('blocked');
+ });
+
+ it('fails claude-code without llm.models.default and never probes', async () => {
+ let probeCalls = 0;
+ const project = projectWithConfig(withBackendNoModel('claude-code'));
+ const status = await buildProjectStatus(project, {
+ claudeCodeAuthProbe: async () => {
+ probeCalls += 1;
+ return { ok: true };
+ },
+ });
+
+ expect(probeCalls).toBe(0);
+ expect(status.llm.status).toBe('fail');
+ expect(status.llm.detail).toContain('llm.models.default');
+ expect(status.verdict).toBe('blocked');
+ });
+
+ it('fails anthropic without llm.models.default even when the key is set', async () => {
+ const config = withBackendNoModel('anthropic');
+ const project = projectWithConfig({
+ ...config,
+ llm: {
+ ...config.llm,
+ provider: { backend: 'anthropic', anthropic: { api_key: 'env:ANTHROPIC_API_KEY' } }, // pragma: allowlist secret
+ models: {},
+ },
+ });
+ const status = await buildProjectStatus(project, {
+ env: { ANTHROPIC_API_KEY: 'sk-test' }, // pragma: allowlist secret
+ });
+
+ expect(status.llm.status).toBe('fail');
+ expect(status.llm.detail).toContain('llm.models.default');
+ expect(status.verdict).toBe('blocked');
+ });
+});
+
describe('buildLocalStatsStatus', () => {
let tempDir: string;
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 15bc75f3..a3eaad5f 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -158,6 +158,9 @@ importers:
'@notionhq/client':
specifier: ^5.22.0
version: 5.22.0
+ '@openai/codex-sdk':
+ specifier: ^0.133.0
+ version: 0.133.0
ai:
specifier: ^6.0.188
version: 6.0.188(zod@4.4.3)
@@ -1288,6 +1291,51 @@ packages:
'@octokit/types@16.0.0':
resolution: {integrity: sha512-sKq+9r1Mm4efXW1FCk7hFSeJo4QKreL/tTbR0rz/qx/r1Oa2VV83LTA/H/MuCOX7uCIJmQVRKBcbmWoySjAnSg==}
+ '@openai/codex-sdk@0.133.0':
+ resolution: {integrity: sha512-PB82D/1Q0C7nzaV5O+1O4y5LcVwiUvxyHvCUTfz8Cwztv6bOWQ40gFHE5ZFX1EFPJx1cMV0GPVODWuXIKAuayQ==}
+ engines: {node: '>=18'}
+
+ '@openai/codex@0.133.0':
+ resolution: {integrity: sha512-Gh42kLLBo/6gpnHmDzUWDVvyS57ekCB1+1Dz0RG2oIl3Lhk1uwrjSj/PwaJWWh4Rw/rUp1RqkwrMugFfFEOlqQ==}
+ engines: {node: '>=16'}
+ hasBin: true
+
+ '@openai/codex@0.133.0-darwin-arm64':
+ resolution: {integrity: sha512-W7f8+DckLujnqGlptKCzgJU+ooeHKMuk6KYgMFP6A9asn7YUsGUgJqjiBaX8oNcXO6w/pTbKGRARx1kCNS8lIg==}
+ engines: {node: '>=16'}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@openai/codex@0.133.0-darwin-x64':
+ resolution: {integrity: sha512-Ek8ikvLOiXZ8emcIJVBXxK6fm8ratBy0kaEt3JNisTNszxGshUHf/R4xxDxIyKNcUkYYXjW7A/rMwW3iu3OFlg==}
+ engines: {node: '>=16'}
+ cpu: [x64]
+ os: [darwin]
+
+ '@openai/codex@0.133.0-linux-arm64':
+ resolution: {integrity: sha512-uKXYYSJ3mY16sp4hcG/4BMNRjva/ZS4oARiI1+7k8+NiuoAhdCGWNe5u4KJ3sMuL3tp/IXcmc6B56EFX1+WDBQ==}
+ engines: {node: '>=16'}
+ cpu: [arm64]
+ os: [linux]
+
+ '@openai/codex@0.133.0-linux-x64':
+ resolution: {integrity: sha512-9YfyqrfUj/UZ2+aXE4zBz47t6RXbVni95ZorGsNh857vxYK/asVpUtR2cymo9lB3JaI4mQaKFfV/t7IRItqkuA==}
+ engines: {node: '>=16'}
+ cpu: [x64]
+ os: [linux]
+
+ '@openai/codex@0.133.0-win32-arm64':
+ resolution: {integrity: sha512-mRzND0PSGHRoLk0X41GTSoc3tFjZSF4HgDlfjU5fiQcWVi0/kLb7Ku6/tPFT/X2hOLa3YdJkbIcHC0Hc9ni80g==}
+ engines: {node: '>=16'}
+ cpu: [arm64]
+ os: [win32]
+
+ '@openai/codex@0.133.0-win32-x64':
+ resolution: {integrity: sha512-u3ji78DIPZCGJeELuovsAnaZH+vK9gsA4F6M1y+Uy2s80Sz7/i1S0KL81qGReYji3urSjgBpkQuNP47GXOqxrQ==}
+ engines: {node: '>=16'}
+ cpu: [x64]
+ os: [win32]
+
'@opentelemetry/api@1.9.1':
resolution: {integrity: sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q==}
engines: {node: '>=8.0.0'}
@@ -7145,6 +7193,37 @@ snapshots:
dependencies:
'@octokit/openapi-types': 27.0.0
+ '@openai/codex-sdk@0.133.0':
+ dependencies:
+ '@openai/codex': 0.133.0
+
+ '@openai/codex@0.133.0':
+ optionalDependencies:
+ '@openai/codex-darwin-arm64': '@openai/codex@0.133.0-darwin-arm64'
+ '@openai/codex-darwin-x64': '@openai/codex@0.133.0-darwin-x64'
+ '@openai/codex-linux-arm64': '@openai/codex@0.133.0-linux-arm64'
+ '@openai/codex-linux-x64': '@openai/codex@0.133.0-linux-x64'
+ '@openai/codex-win32-arm64': '@openai/codex@0.133.0-win32-arm64'
+ '@openai/codex-win32-x64': '@openai/codex@0.133.0-win32-x64'
+
+ '@openai/codex@0.133.0-darwin-arm64':
+ optional: true
+
+ '@openai/codex@0.133.0-darwin-x64':
+ optional: true
+
+ '@openai/codex@0.133.0-linux-arm64':
+ optional: true
+
+ '@openai/codex@0.133.0-linux-x64':
+ optional: true
+
+ '@openai/codex@0.133.0-win32-arm64':
+ optional: true
+
+ '@openai/codex@0.133.0-win32-x64':
+ optional: true
+
'@opentelemetry/api@1.9.1': {}
'@orama/orama@3.1.18': {}
diff --git a/scripts/codex-backend-live-smoke.mjs b/scripts/codex-backend-live-smoke.mjs
new file mode 100644
index 00000000..7793fefc
--- /dev/null
+++ b/scripts/codex-backend-live-smoke.mjs
@@ -0,0 +1,160 @@
+import { execFile } from 'node:child_process';
+import { mkdtemp, rm } from 'node:fs/promises';
+import { tmpdir } from 'node:os';
+import { dirname, join, resolve } from 'node:path';
+import { fileURLToPath, pathToFileURL } from 'node:url';
+import { promisify } from 'node:util';
+
+const execFileAsync = promisify(execFile);
+const SCRIPT_DIR = dirname(fileURLToPath(import.meta.url));
+const ROOT_DIR = resolve(SCRIPT_DIR, '..');
+const OPT_IN_MESSAGE =
+ 'Set KTX_RUN_CODEX_BACKEND_SMOKE=1 or pass --force to run the Codex backend live smoke.';
+
+export function codexBackendSmokeOptIn(env = process.env, args = process.argv.slice(2)) {
+ if (env.KTX_RUN_CODEX_BACKEND_SMOKE === '1' || args.includes('--force')) {
+ return { run: true };
+ }
+ return { run: false, message: OPT_IN_MESSAGE };
+}
+
+async function run(command, args, options = {}) {
+ process.stdout.write(`$ ${command} ${args.join(' ')}\n`);
+ try {
+ const result = await execFileAsync(command, args, {
+ cwd: options.cwd ?? ROOT_DIR,
+ env: { ...process.env, ...(options.env ?? {}) },
+ encoding: 'utf8',
+ maxBuffer: 1024 * 1024 * 20,
+ timeout: options.timeoutMs ?? 300_000,
+ });
+ if (result.stdout) {
+ process.stdout.write(result.stdout);
+ }
+ if (result.stderr) {
+ process.stderr.write(result.stderr);
+ }
+ return { code: 0, stdout: result.stdout, stderr: result.stderr };
+ } catch (error) {
+ const stdout = typeof error.stdout === 'string' ? error.stdout : '';
+ const stderr = typeof error.stderr === 'string' ? error.stderr : error.message;
+ if (stdout) {
+ process.stdout.write(stdout);
+ }
+ if (stderr) {
+ process.stderr.write(stderr);
+ }
+ return {
+ code: typeof error.code === 'number' ? error.code : 1,
+ stdout,
+ stderr,
+ };
+ }
+}
+
+function requireSuccess(label, result) {
+ if (result.code !== 0) {
+ throw new Error(`${label} failed with code ${result.code}\nstdout:\n${result.stdout}\nstderr:\n${result.stderr}`);
+ }
+}
+
+async function runSetupSmoke(projectDir) {
+ const result = await run(
+ 'node',
+ [
+ join(ROOT_DIR, 'packages/cli/dist/bin.js'),
+ 'setup',
+ '--project-dir',
+ projectDir,
+ '--llm-backend',
+ 'codex',
+ '--llm-model',
+ 'gpt-5.3-codex',
+ '--no-input',
+ '--yes',
+ '--skip-databases',
+ '--skip-sources',
+ '--skip-agents',
+ ],
+ { timeoutMs: 600_000 },
+ );
+ requireSuccess('ktx setup codex backend', result);
+ if (!result.stdout.includes('LLM ready: yes (codex, gpt-5.3-codex)')) {
+ throw new Error(`setup did not report Codex LLM readiness\nstdout:\n${result.stdout}`);
+ }
+}
+
+async function runRuntimeSmoke(projectDir) {
+ const runtimeUrl = pathToFileURL(join(ROOT_DIR, 'packages/cli/dist/context/llm/codex-runtime.js')).href;
+ const zodUrl = pathToFileURL(join(ROOT_DIR, 'packages/cli/node_modules/zod/index.js')).href;
+ const { CodexKtxLlmRuntime } = await import(runtimeUrl);
+ const { z } = await import(zodUrl);
+ const runtime = new CodexKtxLlmRuntime({
+ projectDir,
+ modelSlots: { default: 'gpt-5.3-codex' },
+ });
+
+ const text = await runtime.generateText({
+ role: 'default',
+ prompt: 'Reply with exactly: ktx_codex_text_ok',
+ });
+ if (text.trim() !== 'ktx_codex_text_ok') {
+ throw new Error(`Codex text smoke returned unexpected text: ${text}`);
+ }
+
+ let toolCalls = 0;
+ const loop = await runtime.runAgentLoop({
+ modelRole: 'default',
+ systemPrompt: 'You must use available tools when the user asks for a tool result.',
+ userPrompt:
+ 'Call the echo_value tool with {"value":"ktx_codex_tool_ok"}, then finish after the tool returns.',
+ toolSet: {
+ echo_value: {
+ name: 'echo_value',
+ description: 'Return the provided value as markdown.',
+ inputSchema: z.object({ value: z.string() }),
+ execute: async (input) => {
+ toolCalls += 1;
+ return { markdown: `echo:${input.value}` };
+ },
+ },
+ },
+ stepBudget: 4,
+ telemetryTags: {},
+ });
+
+ if (loop.stopReason !== 'natural') {
+ throw new Error(`Codex tool smoke stopped with ${loop.stopReason}: ${loop.error?.message ?? 'no error'}`);
+ }
+ if (toolCalls !== 1) {
+ throw new Error(`Expected Codex to call echo_value exactly once, got ${toolCalls}`);
+ }
+}
+
+export async function runCodexBackendLiveSmoke() {
+ const projectDir = await mkdtemp(join(tmpdir(), 'ktx-codex-backend-smoke-'));
+ try {
+ requireSuccess(
+ 'ktx build',
+ await run('pnpm', ['--filter', '@kaelio/ktx', 'run', 'build'], { timeoutMs: 600_000 }),
+ );
+ await runSetupSmoke(projectDir);
+ await runRuntimeSmoke(projectDir);
+ process.stdout.write(`Codex backend live smoke passed in ${projectDir}\n`);
+ } finally {
+ await rm(projectDir, { recursive: true, force: true });
+ }
+}
+
+async function main() {
+ const optIn = codexBackendSmokeOptIn();
+ if (!optIn.run) {
+ process.stdout.write(`${optIn.message}\n`);
+ return;
+ }
+ await runCodexBackendLiveSmoke();
+}
+
+if (import.meta.url === pathToFileURL(process.argv[1] ?? '').href) {
+ await main();
+}
diff --git a/scripts/codex-backend-live-smoke.test.mjs b/scripts/codex-backend-live-smoke.test.mjs
new file mode 100644
index 00000000..8d8c051f
--- /dev/null
+++ b/scripts/codex-backend-live-smoke.test.mjs
@@ -0,0 +1,18 @@
+import assert from 'node:assert/strict';
+import test from 'node:test';
+import { codexBackendSmokeOptIn } from './codex-backend-live-smoke.mjs';
+
+test('codex backend smoke stays disabled by default', () => {
+ assert.deepEqual(codexBackendSmokeOptIn({}, []), {
+ run: false,
+ message: 'Set KTX_RUN_CODEX_BACKEND_SMOKE=1 or pass --force to run the Codex backend live smoke.',
+ });
+});
+
+test('codex backend smoke runs with env opt-in', () => {
+ assert.deepEqual(codexBackendSmokeOptIn({ KTX_RUN_CODEX_BACKEND_SMOKE: '1' }, []), { run: true });
+});
+
+test('codex backend smoke runs with force flag', () => {
+ assert.deepEqual(codexBackendSmokeOptIn({}, ['--force']), { run: true });
+});
From 6da8c3452a97bfcbeefd8bbcc3379d4d41b4dc9f Mon Sep 17 00:00:00 2001
From: Andrey Avtomonov
Date: Tue, 2 Jun 2026 17:23:51 +0200
Subject: [PATCH 06/49] feat(telemetry): include error details for failures
(#254)
---
.../content/docs/community/telemetry.mdx | 20 +-
packages/cli/src/connection.ts | 4 +-
packages/cli/src/public-ingest.ts | 5 +
packages/cli/src/scan.ts | 4 +-
packages/cli/src/setup-context.ts | 5 +-
packages/cli/src/setup.ts | 5 +-
packages/cli/src/telemetry/command-hook.ts | 5 +-
packages/cli/src/telemetry/events.schema.json | 31 +-
packages/cli/src/telemetry/events.ts | 12 +-
packages/cli/src/telemetry/scrubber.ts | 24 +
packages/cli/test/connection.test.ts | 21 +
packages/cli/test/public-ingest.test.ts | 26 +
packages/cli/test/scan.test.ts | 31 +
packages/cli/test/setup-context.test.ts | 24 +
.../cli/test/telemetry/command-hook.test.ts | 19 +
packages/cli/test/telemetry/scrubber.test.ts | 38 +-
.../ktx_daemon/telemetry/events.schema.json | 31 +-
uv.lock | 1953 +++++++++--------
18 files changed, 1259 insertions(+), 999 deletions(-)
diff --git a/docs-site/content/docs/community/telemetry.mdx b/docs-site/content/docs/community/telemetry.mdx
index c2a9af21..9618af8c 100644
--- a/docs-site/content/docs/community/telemetry.mdx
+++ b/docs-site/content/docs/community/telemetry.mdx
@@ -25,10 +25,11 @@ Use any of these mechanisms to disable telemetry:
## What we collect
-High-level signals only: which commands run, how long they take, whether they
+High-level signals: which commands run, how long they take, whether they
succeed or fail, and basic environment metadata (CLI version, Node version, OS
-platform). For project-level analysis, **ktx** sends a salted hash of the
-project directory — never the raw path.
+platform). When an operation fails, we also include diagnostic detail about the
+error so we can debug it. For project-level analysis, **ktx** sends a salted
+hash of the project directory to group events.
When an agent reaches **ktx** through MCP, we also record the connecting client
tool's self-reported name and version (for example Claude Desktop, Cursor, or
@@ -37,11 +38,14 @@ tool, never you or your data.
## What we never collect
-- File paths, hostnames, environment variable values, or command arguments
-- `ktx.yaml` contents, connection passwords, API keys, or tokens
-- Schema names, table names, column names, SQL text, or query results
-- Error messages or stack traces
-- Git remote URLs, Git user email, OS user, or hostname
+We build telemetry around counts and coarse signals, not the contents of your
+data or configuration. We don't deliberately collect your `ktx.yaml`, query
+results, passwords, API keys, or access tokens.
+
+The one place environment-specific text can appear is failure diagnostics: when
+an operation errors, the detail we record is the error as your tools reported
+it, which can include identifiers from your setup. If you'd rather send nothing
+at all, turn telemetry off using any of the options above.
## Storage and retention
diff --git a/packages/cli/src/connection.ts b/packages/cli/src/connection.ts
index abc501a6..96281e82 100644
--- a/packages/cli/src/connection.ts
+++ b/packages/cli/src/connection.ts
@@ -17,7 +17,7 @@ import { createKtxCliScanConnector } from './local-scan-connectors.js';
import { profileMark } from './startup-profile.js';
import { isDemoConnection } from './telemetry/demo-detect.js';
import { emitTelemetryEvent } from './telemetry/index.js';
-import { scrubErrorClass } from './telemetry/scrubber.js';
+import { formatErrorDetail, scrubErrorClass } from './telemetry/scrubber.js';
profileMark('module:connection');
@@ -304,6 +304,7 @@ async function emitConnectionTest(input: {
io: KtxCliIo;
}): Promise {
const errorClass = input.error ? scrubErrorClass(input.error) : undefined;
+ const errorDetail = input.error ? formatErrorDetail(input.error) : undefined;
await emitTelemetryEvent({
name: 'connection_test',
projectDir: input.project.projectDir,
@@ -314,6 +315,7 @@ async function emitConnectionTest(input: {
outcome: input.outcome,
durationMs: input.durationMs,
...(errorClass ? { errorClass } : {}),
+ ...(errorDetail ? { errorDetail } : {}),
},
});
}
diff --git a/packages/cli/src/public-ingest.ts b/packages/cli/src/public-ingest.ts
index f2b8cdd4..216d1d7b 100644
--- a/packages/cli/src/public-ingest.ts
+++ b/packages/cli/src/public-ingest.ts
@@ -22,6 +22,7 @@ import type { KtxScanArgs, KtxScanDeps } from './scan.js';
import { profileMark } from './startup-profile.js';
import { isDemoConnection } from './telemetry/demo-detect.js';
import { emitProjectStackSnapshot, emitTelemetryEvent } from './telemetry/index.js';
+import { formatErrorDetail } from './telemetry/scrubber.js';
profileMark('module:public-ingest');
@@ -635,6 +636,9 @@ async function emitIngestCompleted(input: {
io: KtxCliIo;
}): Promise {
const failed = resultFailed(input.result);
+ const failureDetail = failed
+ ? formatErrorDetail(input.result.steps.find((step) => step.status === 'failed')?.detail)
+ : undefined;
await emitTelemetryEvent({
name: 'ingest_completed',
projectDir: input.args.projectDir,
@@ -651,6 +655,7 @@ async function emitIngestCompleted(input: {
rowsBucket: rowsBucket(),
durationMs: Math.max(0, performance.now() - input.startedAt),
outcome: failed ? 'error' : 'ok',
+ ...(failureDetail ? { errorDetail: failureDetail } : {}),
},
});
}
diff --git a/packages/cli/src/scan.ts b/packages/cli/src/scan.ts
index 94b80f65..4f973e57 100644
--- a/packages/cli/src/scan.ts
+++ b/packages/cli/src/scan.ts
@@ -9,7 +9,7 @@ import { createKtxCliScanConnector } from './local-scan-connectors.js';
import type { KtxManagedPythonInstallPolicy } from './managed-python-command.js';
import { profileMark } from './startup-profile.js';
import { emitTelemetryEvent } from './telemetry/index.js';
-import { scrubErrorClass } from './telemetry/scrubber.js';
+import { formatErrorDetail, scrubErrorClass } from './telemetry/scrubber.js';
profileMark('module:scan');
@@ -380,6 +380,7 @@ export async function runKtxScan(args: KtxScanArgs, io: KtxCliIo = process, deps
return 0;
} catch (error) {
const errorClass = scrubErrorClass(error);
+ const errorDetail = formatErrorDetail(error);
await emitTelemetryEvent({
name: 'scan_completed',
projectDir: args.projectDir,
@@ -393,6 +394,7 @@ export async function runKtxScan(args: KtxScanArgs, io: KtxCliIo = process, deps
durationMs: Math.max(0, performance.now() - startedAt),
outcome: 'error',
...(errorClass ? { errorClass } : {}),
+ ...(errorDetail ? { errorDetail } : {}),
},
});
io.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
diff --git a/packages/cli/src/setup-context.ts b/packages/cli/src/setup-context.ts
index 63b4dbdf..d6ef2639 100644
--- a/packages/cli/src/setup-context.ts
+++ b/packages/cli/src/setup-context.ts
@@ -6,6 +6,7 @@ import { markKtxSetupStateStepComplete, readKtxSetupState } from './context/proj
import { serializeKtxProjectConfig } from './context/project/config.js';
import type { KtxCliIo } from './cli-runtime.js';
import { errorMessage, writePrefixedLines } from './clack.js';
+import { formatErrorDetail } from './telemetry/scrubber.js';
import { buildPublicIngestPlan } from './public-ingest.js';
import type { KtxManagedPythonInstallPolicy } from './managed-python-command.js';
import {
@@ -67,7 +68,7 @@ export type KtxSetupContextResult =
| { status: 'skipped'; projectDir: string }
| { status: 'back'; projectDir: string }
| { status: 'missing-input'; projectDir: string }
- | { status: 'failed'; projectDir: string };
+ | { status: 'failed'; projectDir: string; errorDetail?: string };
export interface KtxSetupContextStepArgs {
projectDir: string;
@@ -702,6 +703,6 @@ export async function runKtxSetupContextStep(
return await runBuild(args, io, deps, project, targets);
} catch (error) {
writePrefixedLines((chunk) => io.stderr.write(chunk), errorMessage(error));
- return { status: 'failed', projectDir: args.projectDir };
+ return { status: 'failed', projectDir: args.projectDir, errorDetail: formatErrorDetail(error) };
}
}
diff --git a/packages/cli/src/setup.ts b/packages/cli/src/setup.ts
index ebc04c87..f8fc2064 100644
--- a/packages/cli/src/setup.ts
+++ b/packages/cli/src/setup.ts
@@ -217,6 +217,7 @@ async function recordSetupStep(input: {
startedAt: number;
io: KtxCliIo;
cliVersion?: string;
+ errorDetail?: string;
}): Promise {
const { emitTelemetryEvent } = await import('./telemetry/index.js');
await emitTelemetryEvent({
@@ -228,6 +229,7 @@ async function recordSetupStep(input: {
step: input.step,
outcome: setupTelemetryOutcome(input.status),
durationMs: Math.max(0, performance.now() - input.startedAt),
+ ...(input.errorDetail ? { errorDetail: input.errorDetail } : {}),
},
});
}
@@ -683,7 +685,7 @@ async function runKtxSetupInner(args: KtxSetupArgs, io: KtxCliIo, deps: KtxSetup
if (!step) break;
const stepStartedAt = performance.now();
- let stepResult: { status: KtxSetupFlowStatus };
+ let stepResult: { status: KtxSetupFlowStatus; errorDetail?: string };
if (step === 'models') {
const modelRunner =
deps.model ?? ((modelArgs, modelIo) => runKtxSetupAnthropicModelStep(modelArgs, modelIo, deps.modelDeps));
@@ -844,6 +846,7 @@ async function runKtxSetupInner(args: KtxSetupArgs, io: KtxCliIo, deps: KtxSetup
startedAt: stepStartedAt,
io,
cliVersion: args.cliVersion,
+ ...(stepResult.errorDetail ? { errorDetail: stepResult.errorDetail } : {}),
});
if (stepResult.status === 'failed') {
diff --git a/packages/cli/src/telemetry/command-hook.ts b/packages/cli/src/telemetry/command-hook.ts
index e4f003d7..99f8723e 100644
--- a/packages/cli/src/telemetry/command-hook.ts
+++ b/packages/cli/src/telemetry/command-hook.ts
@@ -1,4 +1,4 @@
-import { scrubErrorClass } from './scrubber.js';
+import { formatErrorDetail, scrubErrorClass } from './scrubber.js';
export type CommandOutcome = 'ok' | 'error' | 'aborted';
@@ -16,6 +16,7 @@ export interface CompletedCommandSpan {
durationMs: number;
outcome: CommandOutcome;
errorClass?: string;
+ errorDetail?: string;
flagsPresent: Record;
hasProject: boolean;
projectDir?: string;
@@ -40,12 +41,14 @@ export function completeCommandSpan(input: {
}
const errorClass = input.error ? scrubErrorClass(input.error) : undefined;
+ const errorDetail = input.error ? formatErrorDetail(input.error) : undefined;
return {
commandPath: span.commandPath,
durationMs: Math.max(0, input.completedAt - span.startedAt),
outcome: input.outcome,
...(errorClass ? { errorClass } : {}),
+ ...(errorDetail ? { errorDetail } : {}),
flagsPresent: span.flagsPresent,
hasProject: span.hasProject,
projectDir: span.projectDir,
diff --git a/packages/cli/src/telemetry/events.schema.json b/packages/cli/src/telemetry/events.schema.json
index acad7988..a75f92f1 100644
--- a/packages/cli/src/telemetry/events.schema.json
+++ b/packages/cli/src/telemetry/events.schema.json
@@ -26,6 +26,7 @@
"durationMs",
"outcome",
"errorClass",
+ "errorDetail",
"flagsPresent",
"hasProject",
"projectGroupAttached"
@@ -37,7 +38,8 @@
"fields": [
"step",
"outcome",
- "durationMs"
+ "durationMs",
+ "errorDetail"
]
},
{
@@ -56,6 +58,7 @@
"isDemoConnection",
"outcome",
"errorClass",
+ "errorDetail",
"durationMs",
"serverVersion"
]
@@ -84,7 +87,8 @@
"rowsBucket",
"durationMs",
"outcome",
- "errorClass"
+ "errorClass",
+ "errorDetail"
]
},
{
@@ -98,7 +102,8 @@
"declaredFkCount",
"durationMs",
"outcome",
- "errorClass"
+ "errorClass",
+ "errorDetail"
]
},
{
@@ -296,6 +301,10 @@
"errorClass": {
"type": "string"
},
+ "errorDetail": {
+ "type": "string",
+ "maxLength": 1000
+ },
"flagsPresent": {
"type": "object",
"propertyNames": {
@@ -384,6 +393,10 @@
"durationMs": {
"type": "number",
"minimum": 0
+ },
+ "errorDetail": {
+ "type": "string",
+ "maxLength": 1000
}
},
"required": [
@@ -494,6 +507,10 @@
"errorClass": {
"type": "string"
},
+ "errorDetail": {
+ "type": "string",
+ "maxLength": 1000
+ },
"durationMs": {
"type": "number",
"minimum": 0
@@ -673,6 +690,10 @@
},
"errorClass": {
"type": "string"
+ },
+ "errorDetail": {
+ "type": "string",
+ "maxLength": 1000
}
},
"required": [
@@ -759,6 +780,10 @@
},
"errorClass": {
"type": "string"
+ },
+ "errorDetail": {
+ "type": "string",
+ "maxLength": 1000
}
},
"required": [
diff --git a/packages/cli/src/telemetry/events.ts b/packages/cli/src/telemetry/events.ts
index e751cd70..c4fc2e6f 100644
--- a/packages/cli/src/telemetry/events.ts
+++ b/packages/cli/src/telemetry/events.ts
@@ -21,6 +21,7 @@ const commandSchema = telemetryCommonEnvelopeSchema
durationMs: z.number().nonnegative(),
outcome: z.enum(['ok', 'error', 'aborted']),
errorClass: z.string().optional(),
+ errorDetail: z.string().max(1000).optional(),
flagsPresent: z.record(z.string(), z.boolean()),
hasProject: z.boolean(),
projectGroupAttached: z.boolean(),
@@ -45,6 +46,7 @@ const setupStepSchema = telemetryCommonEnvelopeSchema
]),
outcome: z.enum(['completed', 'skipped', 'abandoned']),
durationMs: z.number().nonnegative(),
+ errorDetail: z.string().max(1000).optional(),
})
.strict();
@@ -61,6 +63,7 @@ const connectionTestSchema = telemetryCommonEnvelopeSchema
isDemoConnection: z.boolean(),
outcome: outcomeSchema,
errorClass: z.string().optional(),
+ errorDetail: z.string().max(1000).optional(),
durationMs: z.number().nonnegative(),
serverVersion: z.string().optional(),
})
@@ -90,6 +93,7 @@ const ingestCompletedSchema = telemetryCommonEnvelopeSchema
durationMs: z.number().nonnegative(),
outcome: outcomeSchema,
errorClass: z.string().optional(),
+ errorDetail: z.string().max(1000).optional(),
})
.strict();
@@ -103,6 +107,7 @@ const scanCompletedSchema = telemetryCommonEnvelopeSchema
durationMs: z.number().nonnegative(),
outcome: outcomeSchema,
errorClass: z.string().optional(),
+ errorDetail: z.string().max(1000).optional(),
})
.strict();
@@ -237,6 +242,7 @@ export const telemetryEventCatalog = [
'durationMs',
'outcome',
'errorClass',
+ 'errorDetail',
'flagsPresent',
'hasProject',
'projectGroupAttached',
@@ -245,7 +251,7 @@ export const telemetryEventCatalog = [
{
name: 'setup_step',
description: 'Emitted after an interactive setup step completes, skips, or aborts.',
- fields: ['step', 'outcome', 'durationMs'],
+ fields: ['step', 'outcome', 'durationMs', 'errorDetail'],
},
{
name: 'connection_added',
@@ -255,7 +261,7 @@ export const telemetryEventCatalog = [
{
name: 'connection_test',
description: 'Emitted after ktx connection test completes.',
- fields: ['driver', 'isDemoConnection', 'outcome', 'errorClass', 'durationMs', 'serverVersion'],
+ fields: ['driver', 'isDemoConnection', 'outcome', 'errorClass', 'errorDetail', 'durationMs', 'serverVersion'],
},
{
name: 'project_stack_snapshot',
@@ -275,6 +281,7 @@ export const telemetryEventCatalog = [
'durationMs',
'outcome',
'errorClass',
+ 'errorDetail',
],
},
{
@@ -289,6 +296,7 @@ export const telemetryEventCatalog = [
'durationMs',
'outcome',
'errorClass',
+ 'errorDetail',
],
},
{
diff --git a/packages/cli/src/telemetry/scrubber.ts b/packages/cli/src/telemetry/scrubber.ts
index 27e41f87..a7b8c393 100644
--- a/packages/cli/src/telemetry/scrubber.ts
+++ b/packages/cli/src/telemetry/scrubber.ts
@@ -26,3 +26,27 @@ export function scrubErrorClass(error: unknown): string | undefined {
return constructorName;
}
+
+const MAX_ERROR_DETAIL_LENGTH = 1000;
+
+/**
+ * Human-readable failure detail for telemetry: the error's `.code` (when
+ * present) prefixed onto its `message`, collapsed to a single line and
+ * length-capped. Captures the message only — never the stack.
+ *
+ * This intentionally forwards raw error text, which can include identifiers from
+ * the user's environment (table/column names, hostnames, usernames), so that
+ * funnel failures are diagnosable. Callers must gate it to the failure path.
+ */
+export function formatErrorDetail(error: unknown): string | undefined {
+ if (error === undefined || error === null) {
+ return undefined;
+ }
+
+ const code = (error as { code?: unknown }).code;
+ const message = error instanceof Error ? error.message : String(error);
+ const prefix = typeof code === 'string' || typeof code === 'number' ? `${code}: ` : '';
+ const detail = `${prefix}${message}`.replace(/\s+/g, ' ').trim();
+
+ return detail.length > 0 ? detail.slice(0, MAX_ERROR_DETAIL_LENGTH) : undefined;
+}
diff --git a/packages/cli/test/connection.test.ts b/packages/cli/test/connection.test.ts
index 59ead362..67e55af8 100644
--- a/packages/cli/test/connection.test.ts
+++ b/packages/cli/test/connection.test.ts
@@ -162,6 +162,27 @@ describe('runKtxConnection', () => {
expect(io.stderr()).not.toContain(projectDir);
});
+ it('records the raw errorDetail in connection_test telemetry when a native test fails', async () => {
+ vi.stubEnv('KTX_TELEMETRY_DEBUG', '1');
+ vi.stubEnv('CI', '');
+ const projectDir = join(tempDir, 'project');
+ await initKtxProject({ projectDir });
+ await writeConnections(projectDir, {
+ warehouse: { driver: 'sqlite' },
+ });
+ const { connector } = nativeConnector('sqlite', { success: false, error: 'database file is unreadable' });
+ const io = makeIo();
+
+ const code = await runKtxConnection({ command: 'test', projectDir, connectionId: 'warehouse' }, io.io, {
+ createScanConnector: vi.fn(async () => connector),
+ });
+
+ expect(code).toBe(1);
+ expect(io.stderr()).toContain('"event":"connection_test"');
+ expect(io.stderr()).toContain('"outcome":"error"');
+ expect(io.stderr()).toContain('"errorDetail":"database file is unreadable"');
+ });
+
it('reports the connector error and still cleans up when native testConnection fails', async () => {
const projectDir = join(tempDir, 'project');
await initKtxProject({ projectDir });
diff --git a/packages/cli/test/public-ingest.test.ts b/packages/cli/test/public-ingest.test.ts
index 2ffbefaf..549756eb 100644
--- a/packages/cli/test/public-ingest.test.ts
+++ b/packages/cli/test/public-ingest.test.ts
@@ -431,6 +431,32 @@ describe('runKtxPublicIngest', () => {
}
});
+ it('records errorDetail in ingest_completed telemetry when a target fails', async () => {
+ vi.stubEnv('KTX_TELEMETRY_DEBUG', '1');
+ vi.stubEnv('CI', '');
+ const projectDir = await mkdtemp(join(tmpdir(), 'ktx-public-ingest-telemetry-fail-'));
+ try {
+ await initKtxProject({ projectDir });
+ const io = makeIo({ isTTY: true });
+ const project = deepReadyProject({
+ warehouse: { driver: 'sqlite', path: join(projectDir, 'warehouse.sqlite') },
+ });
+
+ const code = await runKtxPublicIngest(
+ { command: 'run', projectDir, targetConnectionId: 'warehouse', all: false, json: false, inputMode: 'disabled' },
+ io.io,
+ { loadProject: vi.fn(async () => project), runScan: vi.fn(async () => 1) },
+ );
+
+ expect(code).toBe(1);
+ expect(io.stderr()).toContain('"event":"ingest_completed"');
+ expect(io.stderr()).toContain('"outcome":"error"');
+ expect(io.stderr()).toContain('"errorDetail"');
+ } finally {
+ await rm(projectDir, { recursive: true, force: true });
+ }
+ });
+
it('runs query history after schema ingest with current-run window override', async () => {
const io = makeIo();
const runtimeIo = makeIo({ isTTY: true });
diff --git a/packages/cli/test/scan.test.ts b/packages/cli/test/scan.test.ts
index 837acb10..6a524fba 100644
--- a/packages/cli/test/scan.test.ts
+++ b/packages/cli/test/scan.test.ts
@@ -423,6 +423,37 @@ describe('runKtxScan', () => {
expect(io.stderr()).not.toContain(tempDir);
});
+ it('records the raw errorDetail in scan_completed telemetry when the scan throws', async () => {
+ vi.stubEnv('KTX_TELEMETRY_DEBUG', '1');
+ vi.stubEnv('CI', '');
+ await initKtxProject({ projectDir: tempDir });
+ const runLocalScan = vi.fn(async (): Promise => {
+ const error = new Error('introspection timed out');
+ (error as { code?: unknown }).code = 'ETIMEDOUT';
+ throw error;
+ });
+ const io = makeIo({ isTTY: true });
+
+ const code = await runKtxScan(
+ {
+ command: 'run',
+ projectDir: tempDir,
+ connectionId: 'warehouse',
+ mode: 'structural',
+ detectRelationships: false,
+ dryRun: false,
+ databaseIntrospectionUrl: 'http://127.0.0.1:8765',
+ },
+ io.io,
+ { runLocalScan, createLocalIngestAdapters: noLocalIngestAdapters },
+ );
+
+ expect(code).toBe(1);
+ expect(io.stderr()).toContain('"event":"scan_completed"');
+ expect(io.stderr()).toContain('"outcome":"error"');
+ expect(io.stderr()).toContain('"errorDetail":"ETIMEDOUT: introspection timed out"');
+ });
+
it('passes KTX daemon options to local ingest adapters when no explicit daemon URL is set', async () => {
await initKtxProject({ projectDir: tempDir });
const createLocalIngestAdapters = vi.fn(() => []);
diff --git a/packages/cli/test/setup-context.test.ts b/packages/cli/test/setup-context.test.ts
index d04e24e1..2655527b 100644
--- a/packages/cli/test/setup-context.test.ts
+++ b/packages/cli/test/setup-context.test.ts
@@ -332,6 +332,30 @@ describe('setup context build state', () => {
});
});
+ it('captures the raw errorDetail on the result when the context build throws', async () => {
+ await writeReadyProject(tempDir);
+ const io = makeIo();
+ const runContextBuildMock = vi.fn>(async () => {
+ throw new Error('managed runtime exited with code 1');
+ });
+
+ await expect(
+ runKtxSetupContextStep(
+ { projectDir: tempDir, inputMode: 'disabled' },
+ io.io,
+ {
+ runIdFactory: () => 'setup-context-local-throw',
+ now: () => new Date('2026-05-09T10:00:00.000Z'),
+ runContextBuild: runContextBuildMock,
+ },
+ ),
+ ).resolves.toEqual({
+ status: 'failed',
+ projectDir: tempDir,
+ errorDetail: 'managed runtime exited with code 1',
+ });
+ });
+
it('marks context complete without prompting when initial source ingest already made agent context', async () => {
await writeReadyProject(tempDir);
await mkdir(join(tempDir, 'semantic-layer', 'dbt-main'), { recursive: true });
diff --git a/packages/cli/test/telemetry/command-hook.test.ts b/packages/cli/test/telemetry/command-hook.test.ts
index 92105151..63909ac6 100644
--- a/packages/cli/test/telemetry/command-hook.test.ts
+++ b/packages/cli/test/telemetry/command-hook.test.ts
@@ -34,4 +34,23 @@ describe('telemetry command hook', () => {
resetCommandSpan();
expect(completeCommandSpan({ completedAt: 200, outcome: 'ok' })).toBeUndefined();
});
+
+ it('captures errorClass and raw errorDetail on a failed command', () => {
+ resetCommandSpan();
+ beginCommandSpan({
+ commandPath: ['ktx', 'ingest'],
+ flagsPresent: {},
+ hasProject: true,
+ attachProjectGroup: false,
+ startedAt: 0,
+ });
+
+ class KtxConnectionError extends Error {}
+ const error = new KtxConnectionError('connect ECONNREFUSED 127.0.0.1:5432');
+
+ const completed = completeCommandSpan({ completedAt: 10, outcome: 'error', error });
+ expect(completed?.outcome).toBe('error');
+ expect(completed?.errorClass).toBe('KtxConnectionError');
+ expect(completed?.errorDetail).toBe('connect ECONNREFUSED 127.0.0.1:5432');
+ });
});
diff --git a/packages/cli/test/telemetry/scrubber.test.ts b/packages/cli/test/telemetry/scrubber.test.ts
index a12946d4..a6914665 100644
--- a/packages/cli/test/telemetry/scrubber.test.ts
+++ b/packages/cli/test/telemetry/scrubber.test.ts
@@ -1,6 +1,6 @@
import { describe, expect, it } from 'vitest';
-import { scrubErrorClass } from '../../src/telemetry/scrubber.js';
+import { formatErrorDetail, scrubErrorClass } from '../../src/telemetry/scrubber.js';
class KtxProjectMissingAbortError extends Error {}
@@ -23,3 +23,39 @@ describe('scrubErrorClass', () => {
expect(scrubErrorClass(null)).toBeUndefined();
});
});
+
+describe('formatErrorDetail', () => {
+ it('prefixes a string or numeric .code onto the message', () => {
+ const refused = new Error('connect failed');
+ (refused as { code?: unknown }).code = 'ECONNREFUSED';
+ expect(formatErrorDetail(refused)).toBe('ECONNREFUSED: connect failed');
+
+ const forbidden = new Error('forbidden');
+ (forbidden as { code?: unknown }).code = 403;
+ expect(formatErrorDetail(forbidden)).toBe('403: forbidden');
+ });
+
+ it('uses the bare message when there is no .code', () => {
+ expect(formatErrorDetail(new Error('password authentication failed for user "x"'))).toBe(
+ 'password authentication failed for user "x"',
+ );
+ });
+
+ it('accepts non-Error values', () => {
+ expect(formatErrorDetail('boom')).toBe('boom');
+ });
+
+ it('collapses whitespace to a single line', () => {
+ expect(formatErrorDetail(new Error('line one\n line two'))).toBe('line one line two');
+ });
+
+ it('caps the length at 1000 characters', () => {
+ expect(formatErrorDetail(new Error('x'.repeat(2000)))?.length).toBe(1000);
+ });
+
+ it('returns undefined for empty, null, or undefined input', () => {
+ expect(formatErrorDetail(new Error(' '))).toBeUndefined();
+ expect(formatErrorDetail(null)).toBeUndefined();
+ expect(formatErrorDetail(undefined)).toBeUndefined();
+ });
+});
diff --git a/python/ktx-daemon/src/ktx_daemon/telemetry/events.schema.json b/python/ktx-daemon/src/ktx_daemon/telemetry/events.schema.json
index acad7988..a75f92f1 100644
--- a/python/ktx-daemon/src/ktx_daemon/telemetry/events.schema.json
+++ b/python/ktx-daemon/src/ktx_daemon/telemetry/events.schema.json
@@ -26,6 +26,7 @@
"durationMs",
"outcome",
"errorClass",
+ "errorDetail",
"flagsPresent",
"hasProject",
"projectGroupAttached"
@@ -37,7 +38,8 @@
"fields": [
"step",
"outcome",
- "durationMs"
+ "durationMs",
+ "errorDetail"
]
},
{
@@ -56,6 +58,7 @@
"isDemoConnection",
"outcome",
"errorClass",
+ "errorDetail",
"durationMs",
"serverVersion"
]
@@ -84,7 +87,8 @@
"rowsBucket",
"durationMs",
"outcome",
- "errorClass"
+ "errorClass",
+ "errorDetail"
]
},
{
@@ -98,7 +102,8 @@
"declaredFkCount",
"durationMs",
"outcome",
- "errorClass"
+ "errorClass",
+ "errorDetail"
]
},
{
@@ -296,6 +301,10 @@
"errorClass": {
"type": "string"
},
+ "errorDetail": {
+ "type": "string",
+ "maxLength": 1000
+ },
"flagsPresent": {
"type": "object",
"propertyNames": {
@@ -384,6 +393,10 @@
"durationMs": {
"type": "number",
"minimum": 0
+ },
+ "errorDetail": {
+ "type": "string",
+ "maxLength": 1000
}
},
"required": [
@@ -494,6 +507,10 @@
"errorClass": {
"type": "string"
},
+ "errorDetail": {
+ "type": "string",
+ "maxLength": 1000
+ },
"durationMs": {
"type": "number",
"minimum": 0
@@ -673,6 +690,10 @@
},
"errorClass": {
"type": "string"
+ },
+ "errorDetail": {
+ "type": "string",
+ "maxLength": 1000
}
},
"required": [
@@ -759,6 +780,10 @@
},
"errorClass": {
"type": "string"
+ },
+ "errorDetail": {
+ "type": "string",
+ "maxLength": 1000
}
},
"required": [
diff --git a/uv.lock b/uv.lock
index f04683f3..6d00951d 100644
--- a/uv.lock
+++ b/uv.lock
@@ -2,21 +2,21 @@ version = 1
revision = 3
requires-python = ">=3.13"
resolution-markers = [
- "python_full_version >= '3.14' and sys_platform == 'win32'",
- "python_full_version >= '3.14' and sys_platform == 'emscripten'",
- "python_full_version >= '3.14' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32'",
- "python_full_version >= '3.14' and sys_platform == 'darwin'",
- "python_full_version < '3.14' and sys_platform == 'win32'",
- "python_full_version < '3.14' and sys_platform == 'emscripten'",
- "python_full_version < '3.14' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32'",
- "python_full_version < '3.14' and sys_platform == 'darwin'",
+ "python_full_version >= '3.14' and sys_platform == 'win32'",
+ "python_full_version >= '3.14' and sys_platform == 'emscripten'",
+ "python_full_version >= '3.14' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32'",
+ "python_full_version >= '3.14' and sys_platform == 'darwin'",
+ "python_full_version < '3.14' and sys_platform == 'win32'",
+ "python_full_version < '3.14' and sys_platform == 'emscripten'",
+ "python_full_version < '3.14' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32'",
+ "python_full_version < '3.14' and sys_platform == 'darwin'",
]
[manifest]
members = [
- "ktx-daemon",
- "ktx-sl",
- "ktx-workspace",
+ "ktx-daemon",
+ "ktx-sl",
+ "ktx-workspace",
]
[[package]]
@@ -25,7 +25,7 @@ version = "0.0.4"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/57/ba/046ceea27344560984e26a590f90bc7f4a75b06701f653222458922b558c/annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4", size = 7288, upload-time = "2025-11-10T22:07:42.062Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303, upload-time = "2025-11-10T22:07:40.673Z" },
+ { url = "https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303, upload-time = "2025-11-10T22:07:40.673Z" },
]
[[package]]
@@ -34,7 +34,7 @@ version = "0.7.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" },
+ { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" },
]
[[package]]
@@ -42,11 +42,11 @@ name = "anyio"
version = "4.13.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
- { name = "idna" },
+ { name = "idna" },
]
sdist = { url = "https://files.pythonhosted.org/packages/19/14/2c5dd9f512b66549ae92767a9c7b330ae88e1932ca57876909410251fe13/anyio-4.13.0.tar.gz", hash = "sha256:334b70e641fd2221c1505b3890c69882fe4a2df910cba14d97019b90b24439dc", size = 231622, upload-time = "2026-03-24T12:59:09.671Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/da/42/e921fccf5015463e32a3cf6ee7f980a6ed0f395ceeaa45060b61d86486c2/anyio-4.13.0-py3-none-any.whl", hash = "sha256:08b310f9e24a9594186fd75b4f73f4a4152069e3853f1ed8bfbf58369f4ad708", size = 114353, upload-time = "2026-03-24T12:59:08.246Z" },
+ { url = "https://files.pythonhosted.org/packages/da/42/e921fccf5015463e32a3cf6ee7f980a6ed0f395ceeaa45060b61d86486c2/anyio-4.13.0-py3-none-any.whl", hash = "sha256:08b310f9e24a9594186fd75b4f73f4a4152069e3853f1ed8bfbf58369f4ad708", size = 114353, upload-time = "2026-03-24T12:59:08.246Z" },
]
[[package]]
@@ -55,7 +55,7 @@ version = "2.2.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/47/d7/5bbeb12c44d7c4f2fb5b56abce497eb5ed9f34d85701de869acedd602619/backoff-2.2.1.tar.gz", hash = "sha256:03f829f5bb1923180821643f8753b0502c3b682293992485b0eef2807afa5cba", size = 17001, upload-time = "2022-10-05T19:19:32.061Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/df/73/b6e24bd22e6720ca8ee9a85a0c4a2971af8497d8f3193fa05390cbd46e09/backoff-2.2.1-py3-none-any.whl", hash = "sha256:63579f9a0628e06278f7e47b7d7d5b6ce20dc65c5e96a6f3ca99a6adca0396e8", size = 15148, upload-time = "2022-10-05T19:19:30.546Z" },
+ { url = "https://files.pythonhosted.org/packages/df/73/b6e24bd22e6720ca8ee9a85a0c4a2971af8497d8f3193fa05390cbd46e09/backoff-2.2.1-py3-none-any.whl", hash = "sha256:63579f9a0628e06278f7e47b7d7d5b6ce20dc65c5e96a6f3ca99a6adca0396e8", size = 15148, upload-time = "2022-10-05T19:19:30.546Z" },
]
[[package]]
@@ -64,7 +64,7 @@ version = "2026.5.20"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f3/ce/ee2ecad540810a79593028e88299baeae54d346cc7a0d94b6199988b89b1/certifi-2026.5.20.tar.gz", hash = "sha256:69dea482ab64caa7b9f6aba1c6bf48bb6a5448d1c0f1b17ab42ad8c763a5344d", size = 135422, upload-time = "2026-05-20T11:46:50.073Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/59/8c/57e832b7af6d7c5abe66eb3fbe3a3a32f4d11ea23a1aa7131371035be991/certifi-2026.5.20-py3-none-any.whl", hash = "sha256:3c52e209ba0a4ad7aebe60436a4ab349c39e1e602e8c134221e546902ad25897", size = 134134, upload-time = "2026-05-20T11:46:48.578Z" },
+ { url = "https://files.pythonhosted.org/packages/59/8c/57e832b7af6d7c5abe66eb3fbe3a3a32f4d11ea23a1aa7131371035be991/certifi-2026.5.20-py3-none-any.whl", hash = "sha256:3c52e209ba0a4ad7aebe60436a4ab349c39e1e602e8c134221e546902ad25897", size = 134134, upload-time = "2026-05-20T11:46:48.578Z" },
]
[[package]]
@@ -73,7 +73,7 @@ version = "3.5.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/4e/b5/721b8799b04bf9afe054a3899c6cf4e880fcf8563cc71c15610242490a0c/cfgv-3.5.0.tar.gz", hash = "sha256:d5b1034354820651caa73ede66a6294d6e95c1b00acc5e9b098e917404669132", size = 7334, upload-time = "2025-11-19T20:55:51.612Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl", hash = "sha256:a8dc6b26ad22ff227d2634a65cb388215ce6cc96bbcc5cfde7641ae87e8dacc0", size = 7445, upload-time = "2025-11-19T20:55:50.744Z" },
+ { url = "https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl", hash = "sha256:a8dc6b26ad22ff227d2634a65cb388215ce6cc96bbcc5cfde7641ae87e8dacc0", size = 7445, upload-time = "2025-11-19T20:55:50.744Z" },
]
[[package]]
@@ -82,55 +82,55 @@ version = "3.4.7"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/e7/a1/67fe25fac3c7642725500a3f6cfe5821ad557c3abb11c9d20d12c7008d3e/charset_normalizer-3.4.7.tar.gz", hash = "sha256:ae89db9e5f98a11a4bf50407d4363e7b09b31e55bc117b4f7d80aab97ba009e5", size = 144271, upload-time = "2026-04-02T09:28:39.342Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/c1/3b/66777e39d3ae1ddc77ee606be4ec6d8cbd4c801f65e5a1b6f2b11b8346dd/charset_normalizer-3.4.7-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f496c9c3cc02230093d8330875c4c3cdfc3b73612a5fd921c65d39cbcef08063", size = 309627, upload-time = "2026-04-02T09:26:45.198Z" },
- { url = "https://files.pythonhosted.org/packages/2e/4e/b7f84e617b4854ade48a1b7915c8ccfadeba444d2a18c291f696e37f0d3b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ea948db76d31190bf08bd371623927ee1339d5f2a0b4b1b4a4439a65298703c", size = 207008, upload-time = "2026-04-02T09:26:46.824Z" },
- { url = "https://files.pythonhosted.org/packages/c4/bb/ec73c0257c9e11b268f018f068f5d00aa0ef8c8b09f7753ebd5f2880e248/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a277ab8928b9f299723bc1a2dabb1265911b1a76341f90a510368ca44ad9ab66", size = 228303, upload-time = "2026-04-02T09:26:48.397Z" },
- { url = "https://files.pythonhosted.org/packages/85/fb/32d1f5033484494619f701e719429c69b766bfc4dbc61aa9e9c8c166528b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3bec022aec2c514d9cf199522a802bd007cd588ab17ab2525f20f9c34d067c18", size = 224282, upload-time = "2026-04-02T09:26:49.684Z" },
- { url = "https://files.pythonhosted.org/packages/fa/07/330e3a0dda4c404d6da83b327270906e9654a24f6c546dc886a0eb0ffb23/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e044c39e41b92c845bc815e5ae4230804e8e7bc29e399b0437d64222d92809dd", size = 215595, upload-time = "2026-04-02T09:26:50.915Z" },
- { url = "https://files.pythonhosted.org/packages/e3/7c/fc890655786e423f02556e0216d4b8c6bcb6bdfa890160dc66bf52dee468/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:f495a1652cf3fbab2eb0639776dad966c2fb874d79d87ca07f9d5f059b8bd215", size = 201986, upload-time = "2026-04-02T09:26:52.197Z" },
- { url = "https://files.pythonhosted.org/packages/d8/97/bfb18b3db2aed3b90cf54dc292ad79fdd5ad65c4eae454099475cbeadd0d/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e712b419df8ba5e42b226c510472b37bd57b38e897d3eca5e8cfd410a29fa859", size = 211711, upload-time = "2026-04-02T09:26:53.49Z" },
- { url = "https://files.pythonhosted.org/packages/6f/a5/a581c13798546a7fd557c82614a5c65a13df2157e9ad6373166d2a3e645d/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7804338df6fcc08105c7745f1502ba68d900f45fd770d5bdd5288ddccb8a42d8", size = 210036, upload-time = "2026-04-02T09:26:54.975Z" },
- { url = "https://files.pythonhosted.org/packages/8c/bf/b3ab5bcb478e4193d517644b0fb2bf5497fbceeaa7a1bc0f4d5b50953861/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:481551899c856c704d58119b5025793fa6730adda3571971af568f66d2424bb5", size = 202998, upload-time = "2026-04-02T09:26:56.303Z" },
- { url = "https://files.pythonhosted.org/packages/e7/4e/23efd79b65d314fa320ec6017b4b5834d5c12a58ba4610aa353af2e2f577/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f59099f9b66f0d7145115e6f80dd8b1d847176df89b234a5a6b3f00437aa0832", size = 230056, upload-time = "2026-04-02T09:26:57.554Z" },
- { url = "https://files.pythonhosted.org/packages/b9/9f/1e1941bc3f0e01df116e68dc37a55c4d249df5e6fa77f008841aef68264f/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:f59ad4c0e8f6bba240a9bb85504faa1ab438237199d4cce5f622761507b8f6a6", size = 211537, upload-time = "2026-04-02T09:26:58.843Z" },
- { url = "https://files.pythonhosted.org/packages/80/0f/088cbb3020d44428964a6c97fe1edfb1b9550396bf6d278330281e8b709c/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:3dedcc22d73ec993f42055eff4fcfed9318d1eeb9a6606c55892a26964964e48", size = 226176, upload-time = "2026-04-02T09:27:00.437Z" },
- { url = "https://files.pythonhosted.org/packages/6a/9f/130394f9bbe06f4f63e22641d32fc9b202b7e251c9aef4db044324dac493/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:64f02c6841d7d83f832cd97ccf8eb8a906d06eb95d5276069175c696b024b60a", size = 217723, upload-time = "2026-04-02T09:27:02.021Z" },
- { url = "https://files.pythonhosted.org/packages/73/55/c469897448a06e49f8fa03f6caae97074fde823f432a98f979cc42b90e69/charset_normalizer-3.4.7-cp313-cp313-win32.whl", hash = "sha256:4042d5c8f957e15221d423ba781e85d553722fc4113f523f2feb7b188cc34c5e", size = 148085, upload-time = "2026-04-02T09:27:03.192Z" },
- { url = "https://files.pythonhosted.org/packages/5d/78/1b74c5bbb3f99b77a1715c91b3e0b5bdb6fe302d95ace4f5b1bec37b0167/charset_normalizer-3.4.7-cp313-cp313-win_amd64.whl", hash = "sha256:3946fa46a0cf3e4c8cb1cc52f56bb536310d34f25f01ca9b6c16afa767dab110", size = 158819, upload-time = "2026-04-02T09:27:04.454Z" },
- { url = "https://files.pythonhosted.org/packages/68/86/46bd42279d323deb8687c4a5a811fd548cb7d1de10cf6535d099877a9a9f/charset_normalizer-3.4.7-cp313-cp313-win_arm64.whl", hash = "sha256:80d04837f55fc81da168b98de4f4b797ef007fc8a79ab71c6ec9bc4dd662b15b", size = 147915, upload-time = "2026-04-02T09:27:05.971Z" },
- { url = "https://files.pythonhosted.org/packages/97/c8/c67cb8c70e19ef1960b97b22ed2a1567711de46c4ddf19799923adc836c2/charset_normalizer-3.4.7-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c36c333c39be2dbca264d7803333c896ab8fa7d4d6f0ab7edb7dfd7aea6e98c0", size = 309234, upload-time = "2026-04-02T09:27:07.194Z" },
- { url = "https://files.pythonhosted.org/packages/99/85/c091fdee33f20de70d6c8b522743b6f831a2f1cd3ff86de4c6a827c48a76/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c2aed2e5e41f24ea8ef1590b8e848a79b56f3a5564a65ceec43c9d692dc7d8a", size = 208042, upload-time = "2026-04-02T09:27:08.749Z" },
- { url = "https://files.pythonhosted.org/packages/87/1c/ab2ce611b984d2fd5d86a5a8a19c1ae26acac6bad967da4967562c75114d/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:54523e136b8948060c0fa0bc7b1b50c32c186f2fceee897a495406bb6e311d2b", size = 228706, upload-time = "2026-04-02T09:27:09.951Z" },
- { url = "https://files.pythonhosted.org/packages/a8/29/2b1d2cb00bf085f59d29eb773ce58ec2d325430f8c216804a0a5cd83cbca/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:715479b9a2802ecac752a3b0efa2b0b60285cf962ee38414211abdfccc233b41", size = 224727, upload-time = "2026-04-02T09:27:11.175Z" },
- { url = "https://files.pythonhosted.org/packages/47/5c/032c2d5a07fe4d4855fea851209cca2b6f03ebeb6d4e3afdb3358386a684/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bd6c2a1c7573c64738d716488d2cdd3c00e340e4835707d8fdb8dc1a66ef164e", size = 215882, upload-time = "2026-04-02T09:27:12.446Z" },
- { url = "https://files.pythonhosted.org/packages/2c/c2/356065d5a8b78ed04499cae5f339f091946a6a74f91e03476c33f0ab7100/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:c45e9440fb78f8ddabcf714b68f936737a121355bf59f3907f4e17721b9d1aae", size = 200860, upload-time = "2026-04-02T09:27:13.721Z" },
- { url = "https://files.pythonhosted.org/packages/0c/cd/a32a84217ced5039f53b29f460962abb2d4420def55afabe45b1c3c7483d/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3534e7dcbdcf757da6b85a0bbf5b6868786d5982dd959b065e65481644817a18", size = 211564, upload-time = "2026-04-02T09:27:15.272Z" },
- { url = "https://files.pythonhosted.org/packages/44/86/58e6f13ce26cc3b8f4a36b94a0f22ae2f00a72534520f4ae6857c4b81f89/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e8ac484bf18ce6975760921bb6148041faa8fef0547200386ea0b52b5d27bf7b", size = 211276, upload-time = "2026-04-02T09:27:16.834Z" },
- { url = "https://files.pythonhosted.org/packages/8f/fe/d17c32dc72e17e155e06883efa84514ca375f8a528ba2546bee73fc4df81/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a5fe03b42827c13cdccd08e6c0247b6a6d4b5e3cdc53fd1749f5896adcdc2356", size = 201238, upload-time = "2026-04-02T09:27:18.229Z" },
- { url = "https://files.pythonhosted.org/packages/6a/29/f33daa50b06525a237451cdb6c69da366c381a3dadcd833fa5676bc468b3/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:2d6eb928e13016cea4f1f21d1e10c1cebd5a421bc57ddf5b1142ae3f86824fab", size = 230189, upload-time = "2026-04-02T09:27:19.445Z" },
- { url = "https://files.pythonhosted.org/packages/b6/6e/52c84015394a6a0bdcd435210a7e944c5f94ea1055f5cc5d56c5fe368e7b/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e74327fb75de8986940def6e8dee4f127cc9752bee7355bb323cc5b2659b6d46", size = 211352, upload-time = "2026-04-02T09:27:20.79Z" },
- { url = "https://files.pythonhosted.org/packages/8c/d7/4353be581b373033fb9198bf1da3cf8f09c1082561e8e922aa7b39bf9fe8/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d6038d37043bced98a66e68d3aa2b6a35505dc01328cd65217cefe82f25def44", size = 227024, upload-time = "2026-04-02T09:27:22.063Z" },
- { url = "https://files.pythonhosted.org/packages/30/45/99d18aa925bd1740098ccd3060e238e21115fffbfdcb8f3ece837d0ace6c/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7579e913a5339fb8fa133f6bbcfd8e6749696206cf05acdbdca71a1b436d8e72", size = 217869, upload-time = "2026-04-02T09:27:23.486Z" },
- { url = "https://files.pythonhosted.org/packages/5c/05/5ee478aa53f4bb7996482153d4bfe1b89e0f087f0ab6b294fcf92d595873/charset_normalizer-3.4.7-cp314-cp314-win32.whl", hash = "sha256:5b77459df20e08151cd6f8b9ef8ef1f961ef73d85c21a555c7eed5b79410ec10", size = 148541, upload-time = "2026-04-02T09:27:25.146Z" },
- { url = "https://files.pythonhosted.org/packages/48/77/72dcb0921b2ce86420b2d79d454c7022bf5be40202a2a07906b9f2a35c97/charset_normalizer-3.4.7-cp314-cp314-win_amd64.whl", hash = "sha256:92a0a01ead5e668468e952e4238cccd7c537364eb7d851ab144ab6627dbbe12f", size = 159634, upload-time = "2026-04-02T09:27:26.642Z" },
- { url = "https://files.pythonhosted.org/packages/c6/a3/c2369911cd72f02386e4e340770f6e158c7980267da16af8f668217abaa0/charset_normalizer-3.4.7-cp314-cp314-win_arm64.whl", hash = "sha256:67f6279d125ca0046a7fd386d01b311c6363844deac3e5b069b514ba3e63c246", size = 148384, upload-time = "2026-04-02T09:27:28.271Z" },
- { url = "https://files.pythonhosted.org/packages/94/09/7e8a7f73d24dba1f0035fbbf014d2c36828fc1bf9c88f84093e57d315935/charset_normalizer-3.4.7-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:effc3f449787117233702311a1b7d8f59cba9ced946ba727bdc329ec69028e24", size = 330133, upload-time = "2026-04-02T09:27:29.474Z" },
- { url = "https://files.pythonhosted.org/packages/8d/da/96975ddb11f8e977f706f45cddd8540fd8242f71ecdb5d18a80723dcf62c/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fbccdc05410c9ee21bbf16a35f4c1d16123dcdeb8a1d38f33654fa21d0234f79", size = 216257, upload-time = "2026-04-02T09:27:30.793Z" },
- { url = "https://files.pythonhosted.org/packages/e5/e8/1d63bf8ef2d388e95c64b2098f45f84758f6d102a087552da1485912637b/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:733784b6d6def852c814bce5f318d25da2ee65dd4839a0718641c696e09a2960", size = 234851, upload-time = "2026-04-02T09:27:32.44Z" },
- { url = "https://files.pythonhosted.org/packages/9b/40/e5ff04233e70da2681fa43969ad6f66ca5611d7e669be0246c4c7aaf6dc8/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a89c23ef8d2c6b27fd200a42aa4ac72786e7c60d40efdc76e6011260b6e949c4", size = 233393, upload-time = "2026-04-02T09:27:34.03Z" },
- { url = "https://files.pythonhosted.org/packages/be/c1/06c6c49d5a5450f76899992f1ee40b41d076aee9279b49cf9974d2f313d5/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c114670c45346afedc0d947faf3c7f701051d2518b943679c8ff88befe14f8e", size = 223251, upload-time = "2026-04-02T09:27:35.369Z" },
- { url = "https://files.pythonhosted.org/packages/2b/9f/f2ff16fb050946169e3e1f82134d107e5d4ae72647ec8a1b1446c148480f/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:a180c5e59792af262bf263b21a3c49353f25945d8d9f70628e73de370d55e1e1", size = 206609, upload-time = "2026-04-02T09:27:36.661Z" },
- { url = "https://files.pythonhosted.org/packages/69/d5/a527c0cd8d64d2eab7459784fb4169a0ac76e5a6fc5237337982fd61347e/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3c9a494bc5ec77d43cea229c4f6db1e4d8fe7e1bbffa8b6f0f0032430ff8ab44", size = 220014, upload-time = "2026-04-02T09:27:38.019Z" },
- { url = "https://files.pythonhosted.org/packages/7e/80/8a7b8104a3e203074dc9aa2c613d4b726c0e136bad1cc734594b02867972/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8d828b6667a32a728a1ad1d93957cdf37489c57b97ae6c4de2860fa749b8fc1e", size = 218979, upload-time = "2026-04-02T09:27:39.37Z" },
- { url = "https://files.pythonhosted.org/packages/02/9a/b759b503d507f375b2b5c153e4d2ee0a75aa215b7f2489cf314f4541f2c0/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cf1493cd8607bec4d8a7b9b004e699fcf8f9103a9284cc94962cb73d20f9d4a3", size = 209238, upload-time = "2026-04-02T09:27:40.722Z" },
- { url = "https://files.pythonhosted.org/packages/c2/4e/0f3f5d47b86bdb79256e7290b26ac847a2832d9a4033f7eb2cd4bcf4bb5b/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0c96c3b819b5c3e9e165495db84d41914d6894d55181d2d108cc1a69bfc9cce0", size = 236110, upload-time = "2026-04-02T09:27:42.33Z" },
- { url = "https://files.pythonhosted.org/packages/96/23/bce28734eb3ed2c91dcf93abeb8a5cf393a7b2749725030bb630e554fdd8/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:752a45dc4a6934060b3b0dab47e04edc3326575f82be64bc4fc293914566503e", size = 219824, upload-time = "2026-04-02T09:27:43.924Z" },
- { url = "https://files.pythonhosted.org/packages/2c/6f/6e897c6984cc4d41af319b077f2f600fc8214eb2fe2d6bcb79141b882400/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:8778f0c7a52e56f75d12dae53ae320fae900a8b9b4164b981b9c5ce059cd1fcb", size = 233103, upload-time = "2026-04-02T09:27:45.348Z" },
- { url = "https://files.pythonhosted.org/packages/76/22/ef7bd0fe480a0ae9b656189ec00744b60933f68b4f42a7bb06589f6f576a/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ce3412fbe1e31eb81ea42f4169ed94861c56e643189e1e75f0041f3fe7020abe", size = 225194, upload-time = "2026-04-02T09:27:46.706Z" },
- { url = "https://files.pythonhosted.org/packages/c5/a7/0e0ab3e0b5bc1219bd80a6a0d4d72ca74d9250cb2382b7c699c147e06017/charset_normalizer-3.4.7-cp314-cp314t-win32.whl", hash = "sha256:c03a41a8784091e67a39648f70c5f97b5b6a37f216896d44d2cdcb82615339a0", size = 159827, upload-time = "2026-04-02T09:27:48.053Z" },
- { url = "https://files.pythonhosted.org/packages/7a/1d/29d32e0fb40864b1f878c7f5a0b343ae676c6e2b271a2d55cc3a152391da/charset_normalizer-3.4.7-cp314-cp314t-win_amd64.whl", hash = "sha256:03853ed82eeebbce3c2abfdbc98c96dc205f32a79627688ac9a27370ea61a49c", size = 174168, upload-time = "2026-04-02T09:27:49.795Z" },
- { url = "https://files.pythonhosted.org/packages/de/32/d92444ad05c7a6e41fb2036749777c163baf7a0301a040cb672d6b2b1ae9/charset_normalizer-3.4.7-cp314-cp314t-win_arm64.whl", hash = "sha256:c35abb8bfff0185efac5878da64c45dafd2b37fb0383add1be155a763c1f083d", size = 153018, upload-time = "2026-04-02T09:27:51.116Z" },
- { url = "https://files.pythonhosted.org/packages/db/8f/61959034484a4a7c527811f4721e75d02d653a35afb0b6054474d8185d4c/charset_normalizer-3.4.7-py3-none-any.whl", hash = "sha256:3dce51d0f5e7951f8bb4900c257dad282f49190fdbebecd4ba99bcc41fef404d", size = 61958, upload-time = "2026-04-02T09:28:37.794Z" },
+ { url = "https://files.pythonhosted.org/packages/c1/3b/66777e39d3ae1ddc77ee606be4ec6d8cbd4c801f65e5a1b6f2b11b8346dd/charset_normalizer-3.4.7-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f496c9c3cc02230093d8330875c4c3cdfc3b73612a5fd921c65d39cbcef08063", size = 309627, upload-time = "2026-04-02T09:26:45.198Z" },
+ { url = "https://files.pythonhosted.org/packages/2e/4e/b7f84e617b4854ade48a1b7915c8ccfadeba444d2a18c291f696e37f0d3b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ea948db76d31190bf08bd371623927ee1339d5f2a0b4b1b4a4439a65298703c", size = 207008, upload-time = "2026-04-02T09:26:46.824Z" },
+ { url = "https://files.pythonhosted.org/packages/c4/bb/ec73c0257c9e11b268f018f068f5d00aa0ef8c8b09f7753ebd5f2880e248/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a277ab8928b9f299723bc1a2dabb1265911b1a76341f90a510368ca44ad9ab66", size = 228303, upload-time = "2026-04-02T09:26:48.397Z" },
+ { url = "https://files.pythonhosted.org/packages/85/fb/32d1f5033484494619f701e719429c69b766bfc4dbc61aa9e9c8c166528b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3bec022aec2c514d9cf199522a802bd007cd588ab17ab2525f20f9c34d067c18", size = 224282, upload-time = "2026-04-02T09:26:49.684Z" },
+ { url = "https://files.pythonhosted.org/packages/fa/07/330e3a0dda4c404d6da83b327270906e9654a24f6c546dc886a0eb0ffb23/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e044c39e41b92c845bc815e5ae4230804e8e7bc29e399b0437d64222d92809dd", size = 215595, upload-time = "2026-04-02T09:26:50.915Z" },
+ { url = "https://files.pythonhosted.org/packages/e3/7c/fc890655786e423f02556e0216d4b8c6bcb6bdfa890160dc66bf52dee468/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:f495a1652cf3fbab2eb0639776dad966c2fb874d79d87ca07f9d5f059b8bd215", size = 201986, upload-time = "2026-04-02T09:26:52.197Z" },
+ { url = "https://files.pythonhosted.org/packages/d8/97/bfb18b3db2aed3b90cf54dc292ad79fdd5ad65c4eae454099475cbeadd0d/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e712b419df8ba5e42b226c510472b37bd57b38e897d3eca5e8cfd410a29fa859", size = 211711, upload-time = "2026-04-02T09:26:53.49Z" },
+ { url = "https://files.pythonhosted.org/packages/6f/a5/a581c13798546a7fd557c82614a5c65a13df2157e9ad6373166d2a3e645d/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7804338df6fcc08105c7745f1502ba68d900f45fd770d5bdd5288ddccb8a42d8", size = 210036, upload-time = "2026-04-02T09:26:54.975Z" },
+ { url = "https://files.pythonhosted.org/packages/8c/bf/b3ab5bcb478e4193d517644b0fb2bf5497fbceeaa7a1bc0f4d5b50953861/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:481551899c856c704d58119b5025793fa6730adda3571971af568f66d2424bb5", size = 202998, upload-time = "2026-04-02T09:26:56.303Z" },
+ { url = "https://files.pythonhosted.org/packages/e7/4e/23efd79b65d314fa320ec6017b4b5834d5c12a58ba4610aa353af2e2f577/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f59099f9b66f0d7145115e6f80dd8b1d847176df89b234a5a6b3f00437aa0832", size = 230056, upload-time = "2026-04-02T09:26:57.554Z" },
+ { url = "https://files.pythonhosted.org/packages/b9/9f/1e1941bc3f0e01df116e68dc37a55c4d249df5e6fa77f008841aef68264f/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:f59ad4c0e8f6bba240a9bb85504faa1ab438237199d4cce5f622761507b8f6a6", size = 211537, upload-time = "2026-04-02T09:26:58.843Z" },
+ { url = "https://files.pythonhosted.org/packages/80/0f/088cbb3020d44428964a6c97fe1edfb1b9550396bf6d278330281e8b709c/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:3dedcc22d73ec993f42055eff4fcfed9318d1eeb9a6606c55892a26964964e48", size = 226176, upload-time = "2026-04-02T09:27:00.437Z" },
+ { url = "https://files.pythonhosted.org/packages/6a/9f/130394f9bbe06f4f63e22641d32fc9b202b7e251c9aef4db044324dac493/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:64f02c6841d7d83f832cd97ccf8eb8a906d06eb95d5276069175c696b024b60a", size = 217723, upload-time = "2026-04-02T09:27:02.021Z" },
+ { url = "https://files.pythonhosted.org/packages/73/55/c469897448a06e49f8fa03f6caae97074fde823f432a98f979cc42b90e69/charset_normalizer-3.4.7-cp313-cp313-win32.whl", hash = "sha256:4042d5c8f957e15221d423ba781e85d553722fc4113f523f2feb7b188cc34c5e", size = 148085, upload-time = "2026-04-02T09:27:03.192Z" },
+ { url = "https://files.pythonhosted.org/packages/5d/78/1b74c5bbb3f99b77a1715c91b3e0b5bdb6fe302d95ace4f5b1bec37b0167/charset_normalizer-3.4.7-cp313-cp313-win_amd64.whl", hash = "sha256:3946fa46a0cf3e4c8cb1cc52f56bb536310d34f25f01ca9b6c16afa767dab110", size = 158819, upload-time = "2026-04-02T09:27:04.454Z" },
+ { url = "https://files.pythonhosted.org/packages/68/86/46bd42279d323deb8687c4a5a811fd548cb7d1de10cf6535d099877a9a9f/charset_normalizer-3.4.7-cp313-cp313-win_arm64.whl", hash = "sha256:80d04837f55fc81da168b98de4f4b797ef007fc8a79ab71c6ec9bc4dd662b15b", size = 147915, upload-time = "2026-04-02T09:27:05.971Z" },
+ { url = "https://files.pythonhosted.org/packages/97/c8/c67cb8c70e19ef1960b97b22ed2a1567711de46c4ddf19799923adc836c2/charset_normalizer-3.4.7-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c36c333c39be2dbca264d7803333c896ab8fa7d4d6f0ab7edb7dfd7aea6e98c0", size = 309234, upload-time = "2026-04-02T09:27:07.194Z" },
+ { url = "https://files.pythonhosted.org/packages/99/85/c091fdee33f20de70d6c8b522743b6f831a2f1cd3ff86de4c6a827c48a76/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c2aed2e5e41f24ea8ef1590b8e848a79b56f3a5564a65ceec43c9d692dc7d8a", size = 208042, upload-time = "2026-04-02T09:27:08.749Z" },
+ { url = "https://files.pythonhosted.org/packages/87/1c/ab2ce611b984d2fd5d86a5a8a19c1ae26acac6bad967da4967562c75114d/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:54523e136b8948060c0fa0bc7b1b50c32c186f2fceee897a495406bb6e311d2b", size = 228706, upload-time = "2026-04-02T09:27:09.951Z" },
+ { url = "https://files.pythonhosted.org/packages/a8/29/2b1d2cb00bf085f59d29eb773ce58ec2d325430f8c216804a0a5cd83cbca/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:715479b9a2802ecac752a3b0efa2b0b60285cf962ee38414211abdfccc233b41", size = 224727, upload-time = "2026-04-02T09:27:11.175Z" },
+ { url = "https://files.pythonhosted.org/packages/47/5c/032c2d5a07fe4d4855fea851209cca2b6f03ebeb6d4e3afdb3358386a684/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bd6c2a1c7573c64738d716488d2cdd3c00e340e4835707d8fdb8dc1a66ef164e", size = 215882, upload-time = "2026-04-02T09:27:12.446Z" },
+ { url = "https://files.pythonhosted.org/packages/2c/c2/356065d5a8b78ed04499cae5f339f091946a6a74f91e03476c33f0ab7100/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:c45e9440fb78f8ddabcf714b68f936737a121355bf59f3907f4e17721b9d1aae", size = 200860, upload-time = "2026-04-02T09:27:13.721Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/cd/a32a84217ced5039f53b29f460962abb2d4420def55afabe45b1c3c7483d/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3534e7dcbdcf757da6b85a0bbf5b6868786d5982dd959b065e65481644817a18", size = 211564, upload-time = "2026-04-02T09:27:15.272Z" },
+ { url = "https://files.pythonhosted.org/packages/44/86/58e6f13ce26cc3b8f4a36b94a0f22ae2f00a72534520f4ae6857c4b81f89/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e8ac484bf18ce6975760921bb6148041faa8fef0547200386ea0b52b5d27bf7b", size = 211276, upload-time = "2026-04-02T09:27:16.834Z" },
+ { url = "https://files.pythonhosted.org/packages/8f/fe/d17c32dc72e17e155e06883efa84514ca375f8a528ba2546bee73fc4df81/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a5fe03b42827c13cdccd08e6c0247b6a6d4b5e3cdc53fd1749f5896adcdc2356", size = 201238, upload-time = "2026-04-02T09:27:18.229Z" },
+ { url = "https://files.pythonhosted.org/packages/6a/29/f33daa50b06525a237451cdb6c69da366c381a3dadcd833fa5676bc468b3/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:2d6eb928e13016cea4f1f21d1e10c1cebd5a421bc57ddf5b1142ae3f86824fab", size = 230189, upload-time = "2026-04-02T09:27:19.445Z" },
+ { url = "https://files.pythonhosted.org/packages/b6/6e/52c84015394a6a0bdcd435210a7e944c5f94ea1055f5cc5d56c5fe368e7b/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e74327fb75de8986940def6e8dee4f127cc9752bee7355bb323cc5b2659b6d46", size = 211352, upload-time = "2026-04-02T09:27:20.79Z" },
+ { url = "https://files.pythonhosted.org/packages/8c/d7/4353be581b373033fb9198bf1da3cf8f09c1082561e8e922aa7b39bf9fe8/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d6038d37043bced98a66e68d3aa2b6a35505dc01328cd65217cefe82f25def44", size = 227024, upload-time = "2026-04-02T09:27:22.063Z" },
+ { url = "https://files.pythonhosted.org/packages/30/45/99d18aa925bd1740098ccd3060e238e21115fffbfdcb8f3ece837d0ace6c/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7579e913a5339fb8fa133f6bbcfd8e6749696206cf05acdbdca71a1b436d8e72", size = 217869, upload-time = "2026-04-02T09:27:23.486Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/05/5ee478aa53f4bb7996482153d4bfe1b89e0f087f0ab6b294fcf92d595873/charset_normalizer-3.4.7-cp314-cp314-win32.whl", hash = "sha256:5b77459df20e08151cd6f8b9ef8ef1f961ef73d85c21a555c7eed5b79410ec10", size = 148541, upload-time = "2026-04-02T09:27:25.146Z" },
+ { url = "https://files.pythonhosted.org/packages/48/77/72dcb0921b2ce86420b2d79d454c7022bf5be40202a2a07906b9f2a35c97/charset_normalizer-3.4.7-cp314-cp314-win_amd64.whl", hash = "sha256:92a0a01ead5e668468e952e4238cccd7c537364eb7d851ab144ab6627dbbe12f", size = 159634, upload-time = "2026-04-02T09:27:26.642Z" },
+ { url = "https://files.pythonhosted.org/packages/c6/a3/c2369911cd72f02386e4e340770f6e158c7980267da16af8f668217abaa0/charset_normalizer-3.4.7-cp314-cp314-win_arm64.whl", hash = "sha256:67f6279d125ca0046a7fd386d01b311c6363844deac3e5b069b514ba3e63c246", size = 148384, upload-time = "2026-04-02T09:27:28.271Z" },
+ { url = "https://files.pythonhosted.org/packages/94/09/7e8a7f73d24dba1f0035fbbf014d2c36828fc1bf9c88f84093e57d315935/charset_normalizer-3.4.7-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:effc3f449787117233702311a1b7d8f59cba9ced946ba727bdc329ec69028e24", size = 330133, upload-time = "2026-04-02T09:27:29.474Z" },
+ { url = "https://files.pythonhosted.org/packages/8d/da/96975ddb11f8e977f706f45cddd8540fd8242f71ecdb5d18a80723dcf62c/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fbccdc05410c9ee21bbf16a35f4c1d16123dcdeb8a1d38f33654fa21d0234f79", size = 216257, upload-time = "2026-04-02T09:27:30.793Z" },
+ { url = "https://files.pythonhosted.org/packages/e5/e8/1d63bf8ef2d388e95c64b2098f45f84758f6d102a087552da1485912637b/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:733784b6d6def852c814bce5f318d25da2ee65dd4839a0718641c696e09a2960", size = 234851, upload-time = "2026-04-02T09:27:32.44Z" },
+ { url = "https://files.pythonhosted.org/packages/9b/40/e5ff04233e70da2681fa43969ad6f66ca5611d7e669be0246c4c7aaf6dc8/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a89c23ef8d2c6b27fd200a42aa4ac72786e7c60d40efdc76e6011260b6e949c4", size = 233393, upload-time = "2026-04-02T09:27:34.03Z" },
+ { url = "https://files.pythonhosted.org/packages/be/c1/06c6c49d5a5450f76899992f1ee40b41d076aee9279b49cf9974d2f313d5/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c114670c45346afedc0d947faf3c7f701051d2518b943679c8ff88befe14f8e", size = 223251, upload-time = "2026-04-02T09:27:35.369Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/9f/f2ff16fb050946169e3e1f82134d107e5d4ae72647ec8a1b1446c148480f/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:a180c5e59792af262bf263b21a3c49353f25945d8d9f70628e73de370d55e1e1", size = 206609, upload-time = "2026-04-02T09:27:36.661Z" },
+ { url = "https://files.pythonhosted.org/packages/69/d5/a527c0cd8d64d2eab7459784fb4169a0ac76e5a6fc5237337982fd61347e/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3c9a494bc5ec77d43cea229c4f6db1e4d8fe7e1bbffa8b6f0f0032430ff8ab44", size = 220014, upload-time = "2026-04-02T09:27:38.019Z" },
+ { url = "https://files.pythonhosted.org/packages/7e/80/8a7b8104a3e203074dc9aa2c613d4b726c0e136bad1cc734594b02867972/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8d828b6667a32a728a1ad1d93957cdf37489c57b97ae6c4de2860fa749b8fc1e", size = 218979, upload-time = "2026-04-02T09:27:39.37Z" },
+ { url = "https://files.pythonhosted.org/packages/02/9a/b759b503d507f375b2b5c153e4d2ee0a75aa215b7f2489cf314f4541f2c0/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cf1493cd8607bec4d8a7b9b004e699fcf8f9103a9284cc94962cb73d20f9d4a3", size = 209238, upload-time = "2026-04-02T09:27:40.722Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/4e/0f3f5d47b86bdb79256e7290b26ac847a2832d9a4033f7eb2cd4bcf4bb5b/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0c96c3b819b5c3e9e165495db84d41914d6894d55181d2d108cc1a69bfc9cce0", size = 236110, upload-time = "2026-04-02T09:27:42.33Z" },
+ { url = "https://files.pythonhosted.org/packages/96/23/bce28734eb3ed2c91dcf93abeb8a5cf393a7b2749725030bb630e554fdd8/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:752a45dc4a6934060b3b0dab47e04edc3326575f82be64bc4fc293914566503e", size = 219824, upload-time = "2026-04-02T09:27:43.924Z" },
+ { url = "https://files.pythonhosted.org/packages/2c/6f/6e897c6984cc4d41af319b077f2f600fc8214eb2fe2d6bcb79141b882400/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:8778f0c7a52e56f75d12dae53ae320fae900a8b9b4164b981b9c5ce059cd1fcb", size = 233103, upload-time = "2026-04-02T09:27:45.348Z" },
+ { url = "https://files.pythonhosted.org/packages/76/22/ef7bd0fe480a0ae9b656189ec00744b60933f68b4f42a7bb06589f6f576a/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ce3412fbe1e31eb81ea42f4169ed94861c56e643189e1e75f0041f3fe7020abe", size = 225194, upload-time = "2026-04-02T09:27:46.706Z" },
+ { url = "https://files.pythonhosted.org/packages/c5/a7/0e0ab3e0b5bc1219bd80a6a0d4d72ca74d9250cb2382b7c699c147e06017/charset_normalizer-3.4.7-cp314-cp314t-win32.whl", hash = "sha256:c03a41a8784091e67a39648f70c5f97b5b6a37f216896d44d2cdcb82615339a0", size = 159827, upload-time = "2026-04-02T09:27:48.053Z" },
+ { url = "https://files.pythonhosted.org/packages/7a/1d/29d32e0fb40864b1f878c7f5a0b343ae676c6e2b271a2d55cc3a152391da/charset_normalizer-3.4.7-cp314-cp314t-win_amd64.whl", hash = "sha256:03853ed82eeebbce3c2abfdbc98c96dc205f32a79627688ac9a27370ea61a49c", size = 174168, upload-time = "2026-04-02T09:27:49.795Z" },
+ { url = "https://files.pythonhosted.org/packages/de/32/d92444ad05c7a6e41fb2036749777c163baf7a0301a040cb672d6b2b1ae9/charset_normalizer-3.4.7-cp314-cp314t-win_arm64.whl", hash = "sha256:c35abb8bfff0185efac5878da64c45dafd2b37fb0383add1be155a763c1f083d", size = 153018, upload-time = "2026-04-02T09:27:51.116Z" },
+ { url = "https://files.pythonhosted.org/packages/db/8f/61959034484a4a7c527811f4721e75d02d653a35afb0b6054474d8185d4c/charset_normalizer-3.4.7-py3-none-any.whl", hash = "sha256:3dce51d0f5e7951f8bb4900c257dad282f49190fdbebecd4ba99bcc41fef404d", size = 61958, upload-time = "2026-04-02T09:28:37.794Z" },
]
[[package]]
@@ -138,11 +138,11 @@ name = "click"
version = "8.4.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
- { name = "colorama", marker = "sys_platform == 'win32'" },
+ { name = "colorama", marker = "sys_platform == 'win32'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/9b/98/518d8e5081007684232226f475082b30087d0f585e8457db087298259f49/click-8.4.1.tar.gz", hash = "sha256:918b5633eddf6b41c32d4f454bf0de810065c74e3f7dbf8ee5452f8be88d3e96", size = 353007, upload-time = "2026-05-22T04:08:37.769Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/c7/0d/67e5b4109ea4a837e80daa87c2c696711955e40449a97e8926672534def2/click-8.4.1-py3-none-any.whl", hash = "sha256:482be17c6991b8c19c5429a1e995d9b0efdbb63172824c41f99965dc0ade8ec2", size = 116639, upload-time = "2026-05-22T04:08:35.26Z" },
+ { url = "https://files.pythonhosted.org/packages/c7/0d/67e5b4109ea4a837e80daa87c2c696711955e40449a97e8926672534def2/click-8.4.1-py3-none-any.whl", hash = "sha256:482be17c6991b8c19c5429a1e995d9b0efdbb63172824c41f99965dc0ade8ec2", size = 116639, upload-time = "2026-05-22T04:08:35.26Z" },
]
[[package]]
@@ -151,7 +151,7 @@ version = "0.4.6"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
]
[[package]]
@@ -160,67 +160,67 @@ version = "7.14.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/54/fd/0ab2772530e946e1be1abd0bc09e647ec9b02e88f0867857601fefca8953/coverage-7.14.1.tar.gz", hash = "sha256:30c08f7d90415aa98b3c990385dea2939b0da55f38515e5b369b83655f8523be", size = 920132, upload-time = "2026-05-26T20:41:36.783Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/8a/9e/5f6d56327c62b185225d145191c607e07515294a0aa6338e58805cd4a5ac/coverage-7.14.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:be9f2c802dcfce3f71298303aa5dad0dce440a76c52f2f60dacd8656dab78793", size = 220044, upload-time = "2026-05-26T20:39:29.902Z" },
- { url = "https://files.pythonhosted.org/packages/75/92/e82aca356744cbbc0f77a0b623e38918c1872361963413a3bab5d0340393/coverage-7.14.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6223a72fd0e4c7156353ec0f08a5f93623e1d3034d0e2683b9bb8ea674131b1d", size = 220412, upload-time = "2026-05-26T20:39:31.561Z" },
- { url = "https://files.pythonhosted.org/packages/27/c9/385bde0bf7ed0f4bf3a7ee5367060a86b5d218718cfd6fb943c0f836b34f/coverage-7.14.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7279d2110a28cebc738b6459ecda2771735a4c18465fbbd36b3288fe5ed92247", size = 251412, upload-time = "2026-05-26T20:39:33.337Z" },
- { url = "https://files.pythonhosted.org/packages/51/8c/23faf6a2343a0d17f960a4bd56c43bc7eb4cf312f774dd6ceebd82c7d8fc/coverage-7.14.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9eeb3fcbc13ba40dfbdb22d01d196a28e9cef9ed4c29b60061a1e0e823a9929d", size = 254008, upload-time = "2026-05-26T20:39:35.009Z" },
- { url = "https://files.pythonhosted.org/packages/42/06/36f4aa9ca8a815e6036156e80706a67828bb97bd826948244f6996dda957/coverage-7.14.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f0cfc27c539f07cf5c0a4cfe211d0b6cae039f8f40526dbaa71944e64b50a7b", size = 255241, upload-time = "2026-05-26T20:39:36.71Z" },
- { url = "https://files.pythonhosted.org/packages/ca/79/95266316352f90f6b1c6736bb413302edfde2453fb32422d3911642691b3/coverage-7.14.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:221c70f316241a78e77e607c227cefc8808d4e08f28d99c04f35694690e940be", size = 257373, upload-time = "2026-05-26T20:39:38.412Z" },
- { url = "https://files.pythonhosted.org/packages/e3/9c/58316d1f66c488b5fca8a0eb3e98348807813efa8a0d0833b9021be27488/coverage-7.14.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:da028256b04ec30e5e0114b6f76172938c313991f0a2d3d894271315cf5d5e43", size = 251635, upload-time = "2026-05-26T20:39:40.268Z" },
- { url = "https://files.pythonhosted.org/packages/ef/5a/ca2398a568e16fed7bb713e84ba3603a7164fb65779abe645c565ec890d5/coverage-7.14.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:76a085d7005236a767e3426148b2c407e53ad61695c562f8a81da2d373324901", size = 253373, upload-time = "2026-05-26T20:39:42.145Z" },
- { url = "https://files.pythonhosted.org/packages/6e/2c/0396562c32deaebe7be51d865b3a41e9a87d7561acafe1a28f53b07e019a/coverage-7.14.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b553d04b5e778a8e56d57eb134aff42a92718ecba45e79c4764ecfa40efd92ff", size = 251341, upload-time = "2026-05-26T20:39:43.907Z" },
- { url = "https://files.pythonhosted.org/packages/fd/8f/a94f9221184c9cae1ee115820e3798e48b6b17777a9f19e46fb9a0c8dc74/coverage-7.14.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:46f714d2fb8ae2f4f29f23ada7f1e79b759fff5a70f94a1dac23af204c3ec9e4", size = 255497, upload-time = "2026-05-26T20:39:46.166Z" },
- { url = "https://files.pythonhosted.org/packages/71/69/505d70e47db1eaebcd002c39759707621ef184cd6b1ae084d9f41293f323/coverage-7.14.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:1896f5e19ff3f0431c7ce2172adc54890fd97f86b59ced8ca1649145d9ffe35d", size = 251159, upload-time = "2026-05-26T20:39:48.03Z" },
- { url = "https://files.pythonhosted.org/packages/e0/aa/58681c383aa33a9d2ed40a02d7a22fbf780d1fa4d575396365777828198c/coverage-7.14.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:62fd185ef9df3c33d1c8178c5af105f762afbad96038de9a4ae100aa6297ca33", size = 252934, upload-time = "2026-05-26T20:39:49.872Z" },
- { url = "https://files.pythonhosted.org/packages/eb/fd/11c928cd6bdffc7074bb5965c173d9ebf517fb00205e1da524b98d29ef92/coverage-7.14.1-cp313-cp313-win32.whl", hash = "sha256:ab4af6352741a604c431c6072fce5bee33bf0f20dc7a56618d6bf6bb89e9810c", size = 222584, upload-time = "2026-05-26T20:39:51.68Z" },
- { url = "https://files.pythonhosted.org/packages/6f/92/fb416fc26d340dcba19518c418d6048e913186e17243982c5e435e41fa7a/coverage-7.14.1-cp313-cp313-win_amd64.whl", hash = "sha256:7af486dabe8954d03b087f0021540897afe084f04e16ff5579e08cc46f871416", size = 223394, upload-time = "2026-05-26T20:39:53.472Z" },
- { url = "https://files.pythonhosted.org/packages/73/c6/02d56e3867972f77d5036de924643f26c056e848f00452cafb4dbc3c29b4/coverage-7.14.1-cp313-cp313-win_arm64.whl", hash = "sha256:2224f89ffd0c5605ccce1ed7a584da162bc7c55f601ab1c946bc9de31a486b42", size = 222015, upload-time = "2026-05-26T20:39:55.374Z" },
- { url = "https://files.pythonhosted.org/packages/4d/9e/fcc77914050df73f7662fa1f00902774c79c075a8388ab334074574bf77e/coverage-7.14.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:de286598cc65d2b489411174b1faec2f5a7775fb3201fd925db2a76b4030f37d", size = 220733, upload-time = "2026-05-26T20:39:57.189Z" },
- { url = "https://files.pythonhosted.org/packages/f7/67/2963cbdaf5cbadec44efa3a1e39eaa1f02df4079585f05387607a221e126/coverage-7.14.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:042c46ded7c288aeb07cf14a28b6c1e10b78fcba40171c3fa1e939377eeef0b5", size = 221086, upload-time = "2026-05-26T20:39:59.019Z" },
- { url = "https://files.pythonhosted.org/packages/c8/c5/8701645574e11881f2f47d8930f98bc48b5d43b25eb5b4430dfc4a2f9f48/coverage-7.14.1-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f4ddbe407477f04c45115d1a4e5bc480f753553b534d338d4c3358b1cdd0ea52", size = 262381, upload-time = "2026-05-26T20:40:00.822Z" },
- { url = "https://files.pythonhosted.org/packages/7c/28/7a64d73598263e0c5abd5084211a8474488d31b3c552ff531c719dfcff62/coverage-7.14.1-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d13e6725992e2d2fd7d81d4f5241952d13740121dfd501da09201be39b2c003a", size = 264458, upload-time = "2026-05-26T20:40:02.506Z" },
- { url = "https://files.pythonhosted.org/packages/fa/d8/4969179db9f7eb4df218e69540adf829d1c835f59452513d065d15446802/coverage-7.14.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f747dc8edcfe740130f28f32f3995e955494285717e86ee25af51db2219df08a", size = 266884, upload-time = "2026-05-26T20:40:04.421Z" },
- { url = "https://files.pythonhosted.org/packages/a6/78/a45d5794dbc9bafd97afc96a4377c86c7820d78b6cf51b89bc1d4e919275/coverage-7.14.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ced2f09ef276fd58611a1ef502164ad266d2b75174e5a40cabbdb4033f9f6cf2", size = 268022, upload-time = "2026-05-26T20:40:06.298Z" },
- { url = "https://files.pythonhosted.org/packages/21/cb/4f5e354e9e3e67af96bd4e57113e6db6b22298c7168b13eec408a549903d/coverage-7.14.1-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b84800013769a78ccb9ef4659402e26d06867e337b61ec365f77ad008adea80e", size = 261631, upload-time = "2026-05-26T20:40:08.226Z" },
- { url = "https://files.pythonhosted.org/packages/ec/49/eced49af4cb996d5d8b7e94e736175c513e4facd3398507b89892b4326d8/coverage-7.14.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:ea8cd6ca0ee9f616aaef3afc6882e32c2cbf18b00d96313ffd76af650574034d", size = 264443, upload-time = "2026-05-26T20:40:10.137Z" },
- { url = "https://files.pythonhosted.org/packages/f1/d8/5603a88a7c5913a6b54f6cb1a8c46f7b39cbb30f27cd3f492908da09b2d7/coverage-7.14.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:aa5e304a873fabddc11e484e9b6b738bd38bd7bed17b09aa84eecf5332e8b8bb", size = 262069, upload-time = "2026-05-26T20:40:11.999Z" },
- { url = "https://files.pythonhosted.org/packages/f0/59/2ae3cb79da554a06c8619d6c88ea19dd1e4aed4b834b6a83bb1fa243bdc5/coverage-7.14.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:5a1c5215be81035e629d5bc756650634d0bf31991038db7a0eccb90f025ce16d", size = 265780, upload-time = "2026-05-26T20:40:13.858Z" },
- { url = "https://files.pythonhosted.org/packages/af/5f/b130c1dc999031f2648bd25317fbce505ad8d5562079b4ed81e736a84967/coverage-7.14.1-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:79058c47dae6788504b5effb319961bcd72d7240551464b91d474bc0ed186d69", size = 260970, upload-time = "2026-05-26T20:40:16.142Z" },
- { url = "https://files.pythonhosted.org/packages/87/d1/ec13ccddeb48ec963bdfa72a11224bac2584bd045ba13beca82f8113e9c7/coverage-7.14.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:370c5afae3fa0658e11694a32b24c2778f6bc2d17718121f94ee185e69f26b54", size = 263157, upload-time = "2026-05-26T20:40:18.382Z" },
- { url = "https://files.pythonhosted.org/packages/cf/c2/cd91ead503045161092d3845f7bb95ea2f25131ce96d3e314dd835d91b9c/coverage-7.14.1-cp313-cp313t-win32.whl", hash = "sha256:3758dd0a7f1fa57365ef2e781df0f0731d38b6e3772259d13dae4bd8a958d4b1", size = 223259, upload-time = "2026-05-26T20:40:20.381Z" },
- { url = "https://files.pythonhosted.org/packages/71/9f/1e28d97e6bd2c76b07f38b7c02870f1371255ff6717f54eca578fcbbdd0e/coverage-7.14.1-cp313-cp313t-win_amd64.whl", hash = "sha256:6ff665fb023a77386fe11685190cee1f60a7d635994a30d9b0a061533d470fce", size = 224320, upload-time = "2026-05-26T20:40:22.316Z" },
- { url = "https://files.pythonhosted.org/packages/a9/e0/d936e908f0e1efa55e52b91e01b52f1055cef5e1ab2718493390ed8e2fb8/coverage-7.14.1-cp313-cp313t-win_arm64.whl", hash = "sha256:17a5a241e5997621a956a7f402a7433ef4221e5152809b785bec79e2323799f1", size = 222577, upload-time = "2026-05-26T20:40:24.894Z" },
- { url = "https://files.pythonhosted.org/packages/d6/34/fc2f101b151af3799a101f0550b0454aa008afdc0add677394ec4aa8ea10/coverage-7.14.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d5ed429d0b8edaac649e889b4ffcedb6c80b06629a3f93050e3dddfb99235bee", size = 220091, upload-time = "2026-05-26T20:40:27.249Z" },
- { url = "https://files.pythonhosted.org/packages/3d/a7/1ebae2ab5b961b5c79bb09fe7b3ac99edb190d8be4a8c510b2cf66f46468/coverage-7.14.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8011224a62280e50dab346960c03cf47aca1a1e09e608c0fb33fd6e0cc8e9500", size = 220421, upload-time = "2026-05-26T20:40:30.084Z" },
- { url = "https://files.pythonhosted.org/packages/5e/90/92aca9cf0acc95123c96cd1eb1f08917897a7f5dee01e15738922971ec31/coverage-7.14.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:12c42ec1e14f553c4f817e989365982e646e27211f10a0f717855b94a79c8906", size = 251466, upload-time = "2026-05-26T20:40:32.542Z" },
- { url = "https://files.pythonhosted.org/packages/26/2b/78048cbe3b999f6cbf9cc0d90abba6a88a3e0863a8c1c6cbc762f3f8802f/coverage-7.14.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:06144cd511cf2624873a035c5069cf297144f6e77a73ee3d7a55b605ec5efb42", size = 253973, upload-time = "2026-05-26T20:40:34.473Z" },
- { url = "https://files.pythonhosted.org/packages/8e/21/c2e33b29d1cfde484a19d437afc343c6cd30b08d78cbbf9f5aff14e57b2b/coverage-7.14.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a311d8e1da24be5c1ccf85cbfb06315dbaa1703d5a1eab3f6432c72b837917c8", size = 255318, upload-time = "2026-05-26T20:40:38.154Z" },
- { url = "https://files.pythonhosted.org/packages/8e/ee/aad2f108d63b769121005302f16bf66db8625c88ceaba466942e09a2607e/coverage-7.14.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c79cead5b5bc584d9c71451cb984d0e3a84e0c0937379c8efcbf27c8d661b851", size = 257633, upload-time = "2026-05-26T20:40:40.164Z" },
- { url = "https://files.pythonhosted.org/packages/c2/f8/11a2c29b4fd76d9849f81d0bb812ec0017a9396df3217214e38934a8c837/coverage-7.14.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:dcbf65f1f66a26cdd88c35cf68fb4729c5d1cd2e88added72420541dfb212034", size = 251488, upload-time = "2026-05-26T20:40:42.631Z" },
- { url = "https://files.pythonhosted.org/packages/c9/b8/9a5820de4b8ac2b71d85e3b5fb49108d7469c665f0e2ad0dd7569023e305/coverage-7.14.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fd86572566fb40189a8260446158235159bc7a82dfbc87a3b39cf4fb57fcec1c", size = 253329, upload-time = "2026-05-26T20:40:45.208Z" },
- { url = "https://files.pythonhosted.org/packages/6b/ff/f33e4823667e27548e8fd8df44217515303f9808d0ff29817db56f87d990/coverage-7.14.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:7771b601718fdde84832c3a434ca9bbf4ae9adbc49d84198b4110700c3c77c36", size = 251291, upload-time = "2026-05-26T20:40:47.502Z" },
- { url = "https://files.pythonhosted.org/packages/68/9b/489db0ebb209054766b90a9014a45f6d26eb724c02ec21311c3733b5a644/coverage-7.14.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:39b21e212c55af06fa375e3dbf90a8a8e38792f3a910c580066d23563830ddd5", size = 255564, upload-time = "2026-05-26T20:40:49.372Z" },
- { url = "https://files.pythonhosted.org/packages/27/b5/16bc2d4c2409b23c7737edb68c83bc89e345f378050549fe1d75ac7d34d5/coverage-7.14.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:f2302660e32562a532b442480121aef8aa61a5bdb20b30bf0adab29f10a5a4b4", size = 251107, upload-time = "2026-05-26T20:40:51.677Z" },
- { url = "https://files.pythonhosted.org/packages/7d/0c/2629997469a00cd069d588a41c9dc887610f2775ae89d250c4791e65272a/coverage-7.14.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:03a6f93c1ec3b7f2e77b5dbcc5573a2c21f12529a5c6bbe0f16f72303cc2fa4d", size = 252764, upload-time = "2026-05-26T20:40:54.267Z" },
- { url = "https://files.pythonhosted.org/packages/d2/ee/f78d63c8f079e0d7211c7e2401fa17e311514534ba61bae03e4b287ce4ab/coverage-7.14.1-cp314-cp314-win32.whl", hash = "sha256:8a3ce026d73290f42f08dafecbd82c193a74df280461fbf97300fec51fd133ee", size = 222837, upload-time = "2026-05-26T20:40:56.496Z" },
- { url = "https://files.pythonhosted.org/packages/dc/b9/be539854f93a70dfbeec69117f33ec70dc42ff0b65b5b07ab8d40d04228e/coverage-7.14.1-cp314-cp314-win_amd64.whl", hash = "sha256:114c95ef29302423b87d159075805f4ab973254a2638a5d7d046c94887cc87d7", size = 223650, upload-time = "2026-05-26T20:40:58.351Z" },
- { url = "https://files.pythonhosted.org/packages/fe/9e/24e2842fef40f35ac82ba3a7719c8023d011bf3bf652d0675316a9d088a1/coverage-7.14.1-cp314-cp314-win_arm64.whl", hash = "sha256:a07891c3f4805442b31b71e84ba3cf29ed1aa9a428284e06deeb4b23e5b46343", size = 222218, upload-time = "2026-05-26T20:41:00.321Z" },
- { url = "https://files.pythonhosted.org/packages/0a/1d/ac0a9df5fe31c1e8bdd658074905fc12844a05c1a7e3fdb8417e97c31e23/coverage-7.14.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:1101a5ebb083aecb625ebb6209d4105b58f647b093cb2dc8122d7b33f743cfe1", size = 220822, upload-time = "2026-05-26T20:41:02.281Z" },
- { url = "https://files.pythonhosted.org/packages/32/cf/f964fd9aff20323f9f1a726c97135f8a76bcd87b92dad141a456a43f3c64/coverage-7.14.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:851b9e1e4e8a4608e77c79714b2e77c0970d2ed7202a05e92ae407817481887b", size = 221084, upload-time = "2026-05-26T20:41:04.593Z" },
- { url = "https://files.pythonhosted.org/packages/d8/5e/7e5ef2aba844de2b80d678619fcf0841b42e3f37f16411226f3fe4c1016f/coverage-7.14.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d5b89cdfb2ee051b71e8c3c70bd81a9eff81100f736a269136fe1a68efe00474", size = 262454, upload-time = "2026-05-26T20:41:06.641Z" },
- { url = "https://files.pythonhosted.org/packages/64/62/75809bded87015cc4935524218a2a8ed8dd1a8498bfed30a2f4f7a4b4d34/coverage-7.14.1-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0177614a0370f227888b4e436a7c55686d6a9f90eb1ade2b624ba685a1686e86", size = 264578, upload-time = "2026-05-26T20:41:08.556Z" },
- { url = "https://files.pythonhosted.org/packages/f3/42/d33392dc14633525012d2d504fa1a33b05538bf535f5c1d64675e5754b78/coverage-7.14.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2d69af5dea2de76fc485a83032a630523f985198b7e25be901ec60181587b01e", size = 266981, upload-time = "2026-05-26T20:41:10.824Z" },
- { url = "https://files.pythonhosted.org/packages/2a/49/0157c4428c2aca7f1e09d5565930586fd5ae36f1655f08b0daa7cf1fcae1/coverage-7.14.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:35ab22d91de736e8966b980dc355cbcdd2c6dbbcfe275f9a2991bc8a91b3df65", size = 268112, upload-time = "2026-05-26T20:41:12.966Z" },
- { url = "https://files.pythonhosted.org/packages/96/26/86b9ce71f4092b1ed325ce1421698081df1286b833400b6836912834d6e0/coverage-7.14.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:357d4e32935c36588aaba057d734fa32428c360c9fc2e4442afbf1b646beee6e", size = 261558, upload-time = "2026-05-26T20:41:15Z" },
- { url = "https://files.pythonhosted.org/packages/20/4c/c311210c5472cf5401d8422b0d7812cdd520f24417673afabda6c323faca/coverage-7.14.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:51bd64741cc6fa065abd300ede1afe5a5291ece9c31da8b24884deda48bcc3f8", size = 264447, upload-time = "2026-05-26T20:41:17.369Z" },
- { url = "https://files.pythonhosted.org/packages/fb/71/59513f8710ed3e6b0ac0a050a5b7e977bb9c9e880354863b5d00d8809256/coverage-7.14.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:9132cd363a68a4c3daa7c8704a654b1e39d3360f6f5b8ddd470608a945236c07", size = 262048, upload-time = "2026-05-26T20:41:19.309Z" },
- { url = "https://files.pythonhosted.org/packages/84/8d/bceed32dc494f5bbf50f775cd2e78ca814953942b5ea28d3c1c3ac316f14/coverage-7.14.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:07c6290b1697b862c0478eab545eec949a0d0e4d6d03497f446d706da3b4f2de", size = 265781, upload-time = "2026-05-26T20:41:21.559Z" },
- { url = "https://files.pythonhosted.org/packages/e7/c5/9348fe40dbfd4991aaf78df2c6c3098bfb2cc834d1fd362a64b4efef855a/coverage-7.14.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:5ea0c297e27133853b4d8a3eb799bff5a2dbd9f2f41537a240d337ac9b4df890", size = 260896, upload-time = "2026-05-26T20:41:23.428Z" },
- { url = "https://files.pythonhosted.org/packages/ca/92/1ea0f03929da7cf87206b1fa24f4c8e9c158be0455481af29ec0a1f3503f/coverage-7.14.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:01b7733daad0237daa01ef80fe2dfceffc911e6a17fa7b55d14aa8214eaaaecd", size = 263214, upload-time = "2026-05-26T20:41:25.419Z" },
- { url = "https://files.pythonhosted.org/packages/f6/a9/b2493c054c0e01a643266742ab45e15744e60743f9260cd930c7142b1124/coverage-7.14.1-cp314-cp314t-win32.whl", hash = "sha256:6adc5a36984624a70bf11d7184e20fa0a49aa7c47ffab43804106a1a695ea22e", size = 223624, upload-time = "2026-05-26T20:41:27.795Z" },
- { url = "https://files.pythonhosted.org/packages/fc/bd/3e1e6a57fccd2d7c83fcdf338e93ba98eb85c6e877dd34731ac585375490/coverage-7.14.1-cp314-cp314t-win_amd64.whl", hash = "sha256:ddf799247318f34dbcd2efa8c95a8d0642674e926bb1774cf9b63dfd2a389d1c", size = 224728, upload-time = "2026-05-26T20:41:30.098Z" },
- { url = "https://files.pythonhosted.org/packages/bb/d7/31066cf1d2f0c6c797fce911bcfa01dd35642dc6da992a950256097c5860/coverage-7.14.1-cp314-cp314t-win_arm64.whl", hash = "sha256:145986fe66647eb489f18d9a997567a3fd358584c4b5a808769113abc07466af", size = 222752, upload-time = "2026-05-26T20:41:32.123Z" },
- { url = "https://files.pythonhosted.org/packages/8a/3c/1a983b9a745d7f83d53f057bcc5bf79ba6a2bbc08266b3f0c7d6fe630c9b/coverage-7.14.1-py3-none-any.whl", hash = "sha256:a252f21c27e38347e60111a3266b03827422a7d5525951aceee313aa68bab1d2", size = 211815, upload-time = "2026-05-26T20:41:34.078Z" },
+ { url = "https://files.pythonhosted.org/packages/8a/9e/5f6d56327c62b185225d145191c607e07515294a0aa6338e58805cd4a5ac/coverage-7.14.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:be9f2c802dcfce3f71298303aa5dad0dce440a76c52f2f60dacd8656dab78793", size = 220044, upload-time = "2026-05-26T20:39:29.902Z" },
+ { url = "https://files.pythonhosted.org/packages/75/92/e82aca356744cbbc0f77a0b623e38918c1872361963413a3bab5d0340393/coverage-7.14.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6223a72fd0e4c7156353ec0f08a5f93623e1d3034d0e2683b9bb8ea674131b1d", size = 220412, upload-time = "2026-05-26T20:39:31.561Z" },
+ { url = "https://files.pythonhosted.org/packages/27/c9/385bde0bf7ed0f4bf3a7ee5367060a86b5d218718cfd6fb943c0f836b34f/coverage-7.14.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7279d2110a28cebc738b6459ecda2771735a4c18465fbbd36b3288fe5ed92247", size = 251412, upload-time = "2026-05-26T20:39:33.337Z" },
+ { url = "https://files.pythonhosted.org/packages/51/8c/23faf6a2343a0d17f960a4bd56c43bc7eb4cf312f774dd6ceebd82c7d8fc/coverage-7.14.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9eeb3fcbc13ba40dfbdb22d01d196a28e9cef9ed4c29b60061a1e0e823a9929d", size = 254008, upload-time = "2026-05-26T20:39:35.009Z" },
+ { url = "https://files.pythonhosted.org/packages/42/06/36f4aa9ca8a815e6036156e80706a67828bb97bd826948244f6996dda957/coverage-7.14.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f0cfc27c539f07cf5c0a4cfe211d0b6cae039f8f40526dbaa71944e64b50a7b", size = 255241, upload-time = "2026-05-26T20:39:36.71Z" },
+ { url = "https://files.pythonhosted.org/packages/ca/79/95266316352f90f6b1c6736bb413302edfde2453fb32422d3911642691b3/coverage-7.14.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:221c70f316241a78e77e607c227cefc8808d4e08f28d99c04f35694690e940be", size = 257373, upload-time = "2026-05-26T20:39:38.412Z" },
+ { url = "https://files.pythonhosted.org/packages/e3/9c/58316d1f66c488b5fca8a0eb3e98348807813efa8a0d0833b9021be27488/coverage-7.14.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:da028256b04ec30e5e0114b6f76172938c313991f0a2d3d894271315cf5d5e43", size = 251635, upload-time = "2026-05-26T20:39:40.268Z" },
+ { url = "https://files.pythonhosted.org/packages/ef/5a/ca2398a568e16fed7bb713e84ba3603a7164fb65779abe645c565ec890d5/coverage-7.14.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:76a085d7005236a767e3426148b2c407e53ad61695c562f8a81da2d373324901", size = 253373, upload-time = "2026-05-26T20:39:42.145Z" },
+ { url = "https://files.pythonhosted.org/packages/6e/2c/0396562c32deaebe7be51d865b3a41e9a87d7561acafe1a28f53b07e019a/coverage-7.14.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b553d04b5e778a8e56d57eb134aff42a92718ecba45e79c4764ecfa40efd92ff", size = 251341, upload-time = "2026-05-26T20:39:43.907Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/8f/a94f9221184c9cae1ee115820e3798e48b6b17777a9f19e46fb9a0c8dc74/coverage-7.14.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:46f714d2fb8ae2f4f29f23ada7f1e79b759fff5a70f94a1dac23af204c3ec9e4", size = 255497, upload-time = "2026-05-26T20:39:46.166Z" },
+ { url = "https://files.pythonhosted.org/packages/71/69/505d70e47db1eaebcd002c39759707621ef184cd6b1ae084d9f41293f323/coverage-7.14.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:1896f5e19ff3f0431c7ce2172adc54890fd97f86b59ced8ca1649145d9ffe35d", size = 251159, upload-time = "2026-05-26T20:39:48.03Z" },
+ { url = "https://files.pythonhosted.org/packages/e0/aa/58681c383aa33a9d2ed40a02d7a22fbf780d1fa4d575396365777828198c/coverage-7.14.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:62fd185ef9df3c33d1c8178c5af105f762afbad96038de9a4ae100aa6297ca33", size = 252934, upload-time = "2026-05-26T20:39:49.872Z" },
+ { url = "https://files.pythonhosted.org/packages/eb/fd/11c928cd6bdffc7074bb5965c173d9ebf517fb00205e1da524b98d29ef92/coverage-7.14.1-cp313-cp313-win32.whl", hash = "sha256:ab4af6352741a604c431c6072fce5bee33bf0f20dc7a56618d6bf6bb89e9810c", size = 222584, upload-time = "2026-05-26T20:39:51.68Z" },
+ { url = "https://files.pythonhosted.org/packages/6f/92/fb416fc26d340dcba19518c418d6048e913186e17243982c5e435e41fa7a/coverage-7.14.1-cp313-cp313-win_amd64.whl", hash = "sha256:7af486dabe8954d03b087f0021540897afe084f04e16ff5579e08cc46f871416", size = 223394, upload-time = "2026-05-26T20:39:53.472Z" },
+ { url = "https://files.pythonhosted.org/packages/73/c6/02d56e3867972f77d5036de924643f26c056e848f00452cafb4dbc3c29b4/coverage-7.14.1-cp313-cp313-win_arm64.whl", hash = "sha256:2224f89ffd0c5605ccce1ed7a584da162bc7c55f601ab1c946bc9de31a486b42", size = 222015, upload-time = "2026-05-26T20:39:55.374Z" },
+ { url = "https://files.pythonhosted.org/packages/4d/9e/fcc77914050df73f7662fa1f00902774c79c075a8388ab334074574bf77e/coverage-7.14.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:de286598cc65d2b489411174b1faec2f5a7775fb3201fd925db2a76b4030f37d", size = 220733, upload-time = "2026-05-26T20:39:57.189Z" },
+ { url = "https://files.pythonhosted.org/packages/f7/67/2963cbdaf5cbadec44efa3a1e39eaa1f02df4079585f05387607a221e126/coverage-7.14.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:042c46ded7c288aeb07cf14a28b6c1e10b78fcba40171c3fa1e939377eeef0b5", size = 221086, upload-time = "2026-05-26T20:39:59.019Z" },
+ { url = "https://files.pythonhosted.org/packages/c8/c5/8701645574e11881f2f47d8930f98bc48b5d43b25eb5b4430dfc4a2f9f48/coverage-7.14.1-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f4ddbe407477f04c45115d1a4e5bc480f753553b534d338d4c3358b1cdd0ea52", size = 262381, upload-time = "2026-05-26T20:40:00.822Z" },
+ { url = "https://files.pythonhosted.org/packages/7c/28/7a64d73598263e0c5abd5084211a8474488d31b3c552ff531c719dfcff62/coverage-7.14.1-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d13e6725992e2d2fd7d81d4f5241952d13740121dfd501da09201be39b2c003a", size = 264458, upload-time = "2026-05-26T20:40:02.506Z" },
+ { url = "https://files.pythonhosted.org/packages/fa/d8/4969179db9f7eb4df218e69540adf829d1c835f59452513d065d15446802/coverage-7.14.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f747dc8edcfe740130f28f32f3995e955494285717e86ee25af51db2219df08a", size = 266884, upload-time = "2026-05-26T20:40:04.421Z" },
+ { url = "https://files.pythonhosted.org/packages/a6/78/a45d5794dbc9bafd97afc96a4377c86c7820d78b6cf51b89bc1d4e919275/coverage-7.14.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ced2f09ef276fd58611a1ef502164ad266d2b75174e5a40cabbdb4033f9f6cf2", size = 268022, upload-time = "2026-05-26T20:40:06.298Z" },
+ { url = "https://files.pythonhosted.org/packages/21/cb/4f5e354e9e3e67af96bd4e57113e6db6b22298c7168b13eec408a549903d/coverage-7.14.1-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b84800013769a78ccb9ef4659402e26d06867e337b61ec365f77ad008adea80e", size = 261631, upload-time = "2026-05-26T20:40:08.226Z" },
+ { url = "https://files.pythonhosted.org/packages/ec/49/eced49af4cb996d5d8b7e94e736175c513e4facd3398507b89892b4326d8/coverage-7.14.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:ea8cd6ca0ee9f616aaef3afc6882e32c2cbf18b00d96313ffd76af650574034d", size = 264443, upload-time = "2026-05-26T20:40:10.137Z" },
+ { url = "https://files.pythonhosted.org/packages/f1/d8/5603a88a7c5913a6b54f6cb1a8c46f7b39cbb30f27cd3f492908da09b2d7/coverage-7.14.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:aa5e304a873fabddc11e484e9b6b738bd38bd7bed17b09aa84eecf5332e8b8bb", size = 262069, upload-time = "2026-05-26T20:40:11.999Z" },
+ { url = "https://files.pythonhosted.org/packages/f0/59/2ae3cb79da554a06c8619d6c88ea19dd1e4aed4b834b6a83bb1fa243bdc5/coverage-7.14.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:5a1c5215be81035e629d5bc756650634d0bf31991038db7a0eccb90f025ce16d", size = 265780, upload-time = "2026-05-26T20:40:13.858Z" },
+ { url = "https://files.pythonhosted.org/packages/af/5f/b130c1dc999031f2648bd25317fbce505ad8d5562079b4ed81e736a84967/coverage-7.14.1-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:79058c47dae6788504b5effb319961bcd72d7240551464b91d474bc0ed186d69", size = 260970, upload-time = "2026-05-26T20:40:16.142Z" },
+ { url = "https://files.pythonhosted.org/packages/87/d1/ec13ccddeb48ec963bdfa72a11224bac2584bd045ba13beca82f8113e9c7/coverage-7.14.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:370c5afae3fa0658e11694a32b24c2778f6bc2d17718121f94ee185e69f26b54", size = 263157, upload-time = "2026-05-26T20:40:18.382Z" },
+ { url = "https://files.pythonhosted.org/packages/cf/c2/cd91ead503045161092d3845f7bb95ea2f25131ce96d3e314dd835d91b9c/coverage-7.14.1-cp313-cp313t-win32.whl", hash = "sha256:3758dd0a7f1fa57365ef2e781df0f0731d38b6e3772259d13dae4bd8a958d4b1", size = 223259, upload-time = "2026-05-26T20:40:20.381Z" },
+ { url = "https://files.pythonhosted.org/packages/71/9f/1e28d97e6bd2c76b07f38b7c02870f1371255ff6717f54eca578fcbbdd0e/coverage-7.14.1-cp313-cp313t-win_amd64.whl", hash = "sha256:6ff665fb023a77386fe11685190cee1f60a7d635994a30d9b0a061533d470fce", size = 224320, upload-time = "2026-05-26T20:40:22.316Z" },
+ { url = "https://files.pythonhosted.org/packages/a9/e0/d936e908f0e1efa55e52b91e01b52f1055cef5e1ab2718493390ed8e2fb8/coverage-7.14.1-cp313-cp313t-win_arm64.whl", hash = "sha256:17a5a241e5997621a956a7f402a7433ef4221e5152809b785bec79e2323799f1", size = 222577, upload-time = "2026-05-26T20:40:24.894Z" },
+ { url = "https://files.pythonhosted.org/packages/d6/34/fc2f101b151af3799a101f0550b0454aa008afdc0add677394ec4aa8ea10/coverage-7.14.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d5ed429d0b8edaac649e889b4ffcedb6c80b06629a3f93050e3dddfb99235bee", size = 220091, upload-time = "2026-05-26T20:40:27.249Z" },
+ { url = "https://files.pythonhosted.org/packages/3d/a7/1ebae2ab5b961b5c79bb09fe7b3ac99edb190d8be4a8c510b2cf66f46468/coverage-7.14.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8011224a62280e50dab346960c03cf47aca1a1e09e608c0fb33fd6e0cc8e9500", size = 220421, upload-time = "2026-05-26T20:40:30.084Z" },
+ { url = "https://files.pythonhosted.org/packages/5e/90/92aca9cf0acc95123c96cd1eb1f08917897a7f5dee01e15738922971ec31/coverage-7.14.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:12c42ec1e14f553c4f817e989365982e646e27211f10a0f717855b94a79c8906", size = 251466, upload-time = "2026-05-26T20:40:32.542Z" },
+ { url = "https://files.pythonhosted.org/packages/26/2b/78048cbe3b999f6cbf9cc0d90abba6a88a3e0863a8c1c6cbc762f3f8802f/coverage-7.14.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:06144cd511cf2624873a035c5069cf297144f6e77a73ee3d7a55b605ec5efb42", size = 253973, upload-time = "2026-05-26T20:40:34.473Z" },
+ { url = "https://files.pythonhosted.org/packages/8e/21/c2e33b29d1cfde484a19d437afc343c6cd30b08d78cbbf9f5aff14e57b2b/coverage-7.14.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a311d8e1da24be5c1ccf85cbfb06315dbaa1703d5a1eab3f6432c72b837917c8", size = 255318, upload-time = "2026-05-26T20:40:38.154Z" },
+ { url = "https://files.pythonhosted.org/packages/8e/ee/aad2f108d63b769121005302f16bf66db8625c88ceaba466942e09a2607e/coverage-7.14.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c79cead5b5bc584d9c71451cb984d0e3a84e0c0937379c8efcbf27c8d661b851", size = 257633, upload-time = "2026-05-26T20:40:40.164Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/f8/11a2c29b4fd76d9849f81d0bb812ec0017a9396df3217214e38934a8c837/coverage-7.14.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:dcbf65f1f66a26cdd88c35cf68fb4729c5d1cd2e88added72420541dfb212034", size = 251488, upload-time = "2026-05-26T20:40:42.631Z" },
+ { url = "https://files.pythonhosted.org/packages/c9/b8/9a5820de4b8ac2b71d85e3b5fb49108d7469c665f0e2ad0dd7569023e305/coverage-7.14.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fd86572566fb40189a8260446158235159bc7a82dfbc87a3b39cf4fb57fcec1c", size = 253329, upload-time = "2026-05-26T20:40:45.208Z" },
+ { url = "https://files.pythonhosted.org/packages/6b/ff/f33e4823667e27548e8fd8df44217515303f9808d0ff29817db56f87d990/coverage-7.14.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:7771b601718fdde84832c3a434ca9bbf4ae9adbc49d84198b4110700c3c77c36", size = 251291, upload-time = "2026-05-26T20:40:47.502Z" },
+ { url = "https://files.pythonhosted.org/packages/68/9b/489db0ebb209054766b90a9014a45f6d26eb724c02ec21311c3733b5a644/coverage-7.14.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:39b21e212c55af06fa375e3dbf90a8a8e38792f3a910c580066d23563830ddd5", size = 255564, upload-time = "2026-05-26T20:40:49.372Z" },
+ { url = "https://files.pythonhosted.org/packages/27/b5/16bc2d4c2409b23c7737edb68c83bc89e345f378050549fe1d75ac7d34d5/coverage-7.14.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:f2302660e32562a532b442480121aef8aa61a5bdb20b30bf0adab29f10a5a4b4", size = 251107, upload-time = "2026-05-26T20:40:51.677Z" },
+ { url = "https://files.pythonhosted.org/packages/7d/0c/2629997469a00cd069d588a41c9dc887610f2775ae89d250c4791e65272a/coverage-7.14.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:03a6f93c1ec3b7f2e77b5dbcc5573a2c21f12529a5c6bbe0f16f72303cc2fa4d", size = 252764, upload-time = "2026-05-26T20:40:54.267Z" },
+ { url = "https://files.pythonhosted.org/packages/d2/ee/f78d63c8f079e0d7211c7e2401fa17e311514534ba61bae03e4b287ce4ab/coverage-7.14.1-cp314-cp314-win32.whl", hash = "sha256:8a3ce026d73290f42f08dafecbd82c193a74df280461fbf97300fec51fd133ee", size = 222837, upload-time = "2026-05-26T20:40:56.496Z" },
+ { url = "https://files.pythonhosted.org/packages/dc/b9/be539854f93a70dfbeec69117f33ec70dc42ff0b65b5b07ab8d40d04228e/coverage-7.14.1-cp314-cp314-win_amd64.whl", hash = "sha256:114c95ef29302423b87d159075805f4ab973254a2638a5d7d046c94887cc87d7", size = 223650, upload-time = "2026-05-26T20:40:58.351Z" },
+ { url = "https://files.pythonhosted.org/packages/fe/9e/24e2842fef40f35ac82ba3a7719c8023d011bf3bf652d0675316a9d088a1/coverage-7.14.1-cp314-cp314-win_arm64.whl", hash = "sha256:a07891c3f4805442b31b71e84ba3cf29ed1aa9a428284e06deeb4b23e5b46343", size = 222218, upload-time = "2026-05-26T20:41:00.321Z" },
+ { url = "https://files.pythonhosted.org/packages/0a/1d/ac0a9df5fe31c1e8bdd658074905fc12844a05c1a7e3fdb8417e97c31e23/coverage-7.14.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:1101a5ebb083aecb625ebb6209d4105b58f647b093cb2dc8122d7b33f743cfe1", size = 220822, upload-time = "2026-05-26T20:41:02.281Z" },
+ { url = "https://files.pythonhosted.org/packages/32/cf/f964fd9aff20323f9f1a726c97135f8a76bcd87b92dad141a456a43f3c64/coverage-7.14.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:851b9e1e4e8a4608e77c79714b2e77c0970d2ed7202a05e92ae407817481887b", size = 221084, upload-time = "2026-05-26T20:41:04.593Z" },
+ { url = "https://files.pythonhosted.org/packages/d8/5e/7e5ef2aba844de2b80d678619fcf0841b42e3f37f16411226f3fe4c1016f/coverage-7.14.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d5b89cdfb2ee051b71e8c3c70bd81a9eff81100f736a269136fe1a68efe00474", size = 262454, upload-time = "2026-05-26T20:41:06.641Z" },
+ { url = "https://files.pythonhosted.org/packages/64/62/75809bded87015cc4935524218a2a8ed8dd1a8498bfed30a2f4f7a4b4d34/coverage-7.14.1-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0177614a0370f227888b4e436a7c55686d6a9f90eb1ade2b624ba685a1686e86", size = 264578, upload-time = "2026-05-26T20:41:08.556Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/42/d33392dc14633525012d2d504fa1a33b05538bf535f5c1d64675e5754b78/coverage-7.14.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2d69af5dea2de76fc485a83032a630523f985198b7e25be901ec60181587b01e", size = 266981, upload-time = "2026-05-26T20:41:10.824Z" },
+ { url = "https://files.pythonhosted.org/packages/2a/49/0157c4428c2aca7f1e09d5565930586fd5ae36f1655f08b0daa7cf1fcae1/coverage-7.14.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:35ab22d91de736e8966b980dc355cbcdd2c6dbbcfe275f9a2991bc8a91b3df65", size = 268112, upload-time = "2026-05-26T20:41:12.966Z" },
+ { url = "https://files.pythonhosted.org/packages/96/26/86b9ce71f4092b1ed325ce1421698081df1286b833400b6836912834d6e0/coverage-7.14.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:357d4e32935c36588aaba057d734fa32428c360c9fc2e4442afbf1b646beee6e", size = 261558, upload-time = "2026-05-26T20:41:15Z" },
+ { url = "https://files.pythonhosted.org/packages/20/4c/c311210c5472cf5401d8422b0d7812cdd520f24417673afabda6c323faca/coverage-7.14.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:51bd64741cc6fa065abd300ede1afe5a5291ece9c31da8b24884deda48bcc3f8", size = 264447, upload-time = "2026-05-26T20:41:17.369Z" },
+ { url = "https://files.pythonhosted.org/packages/fb/71/59513f8710ed3e6b0ac0a050a5b7e977bb9c9e880354863b5d00d8809256/coverage-7.14.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:9132cd363a68a4c3daa7c8704a654b1e39d3360f6f5b8ddd470608a945236c07", size = 262048, upload-time = "2026-05-26T20:41:19.309Z" },
+ { url = "https://files.pythonhosted.org/packages/84/8d/bceed32dc494f5bbf50f775cd2e78ca814953942b5ea28d3c1c3ac316f14/coverage-7.14.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:07c6290b1697b862c0478eab545eec949a0d0e4d6d03497f446d706da3b4f2de", size = 265781, upload-time = "2026-05-26T20:41:21.559Z" },
+ { url = "https://files.pythonhosted.org/packages/e7/c5/9348fe40dbfd4991aaf78df2c6c3098bfb2cc834d1fd362a64b4efef855a/coverage-7.14.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:5ea0c297e27133853b4d8a3eb799bff5a2dbd9f2f41537a240d337ac9b4df890", size = 260896, upload-time = "2026-05-26T20:41:23.428Z" },
+ { url = "https://files.pythonhosted.org/packages/ca/92/1ea0f03929da7cf87206b1fa24f4c8e9c158be0455481af29ec0a1f3503f/coverage-7.14.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:01b7733daad0237daa01ef80fe2dfceffc911e6a17fa7b55d14aa8214eaaaecd", size = 263214, upload-time = "2026-05-26T20:41:25.419Z" },
+ { url = "https://files.pythonhosted.org/packages/f6/a9/b2493c054c0e01a643266742ab45e15744e60743f9260cd930c7142b1124/coverage-7.14.1-cp314-cp314t-win32.whl", hash = "sha256:6adc5a36984624a70bf11d7184e20fa0a49aa7c47ffab43804106a1a695ea22e", size = 223624, upload-time = "2026-05-26T20:41:27.795Z" },
+ { url = "https://files.pythonhosted.org/packages/fc/bd/3e1e6a57fccd2d7c83fcdf338e93ba98eb85c6e877dd34731ac585375490/coverage-7.14.1-cp314-cp314t-win_amd64.whl", hash = "sha256:ddf799247318f34dbcd2efa8c95a8d0642674e926bb1774cf9b63dfd2a389d1c", size = 224728, upload-time = "2026-05-26T20:41:30.098Z" },
+ { url = "https://files.pythonhosted.org/packages/bb/d7/31066cf1d2f0c6c797fce911bcfa01dd35642dc6da992a950256097c5860/coverage-7.14.1-cp314-cp314t-win_arm64.whl", hash = "sha256:145986fe66647eb489f18d9a997567a3fd358584c4b5a808769113abc07466af", size = 222752, upload-time = "2026-05-26T20:41:32.123Z" },
+ { url = "https://files.pythonhosted.org/packages/8a/3c/1a983b9a745d7f83d53f057bcc5bf79ba6a2bbc08266b3f0c7d6fe630c9b/coverage-7.14.1-py3-none-any.whl", hash = "sha256:a252f21c27e38347e60111a3266b03827422a7d5525951aceee313aa68bab1d2", size = 211815, upload-time = "2026-05-26T20:41:34.078Z" },
]
[[package]]
@@ -229,7 +229,7 @@ version = "0.4.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/96/8e/709914eb2b5749865801041647dc7f4e6d00b549cfe88b65ca192995f07c/distlib-0.4.0.tar.gz", hash = "sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d", size = 614605, upload-time = "2025-07-17T16:52:00.465Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16", size = 469047, upload-time = "2025-07-17T16:51:58.613Z" },
+ { url = "https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16", size = 469047, upload-time = "2025-07-17T16:51:58.613Z" },
]
[[package]]
@@ -238,7 +238,7 @@ version = "1.9.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/fc/f8/98eea607f65de6527f8a2e8885fc8015d3e6f5775df186e443e0964a11c3/distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed", size = 60722, upload-time = "2023-12-24T09:54:32.31Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277, upload-time = "2023-12-24T09:54:30.421Z" },
+ { url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277, upload-time = "2023-12-24T09:54:30.421Z" },
]
[[package]]
@@ -247,20 +247,20 @@ version = "1.5.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/69/00/d579dcb2a536b6ea3a2563cdad6844f77d81a9b2d4b22a858097f2468acf/duckdb-1.5.3.tar.gz", hash = "sha256:df39428eb130faa35ae96fd35245bdeae6ecf43936250b116b5fead568eb9f16", size = 18026640, upload-time = "2026-05-20T11:55:31.901Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/cc/9c/a528eb09d8be51954c485864bd06753e616939a080cbc3dd4417e8c94a57/duckdb-1.5.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e75a6122c12579a99848517f6f00a4e342aebda3590c30fe9b5cc5f39d5e6afc", size = 32626254, upload-time = "2026-05-20T11:54:53.65Z" },
- { url = "https://files.pythonhosted.org/packages/ec/3c/1534c0a6db347c05eb7d0f6ecfb7aefbe74cbff398e4892a8fd1903a20e8/duckdb-1.5.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:fd3963c1cb9d9567777f4a898a9dbe388a2fe9724681801b1e7d6d93eecf1b76", size = 17300917, upload-time = "2026-05-20T11:54:56.628Z" },
- { url = "https://files.pythonhosted.org/packages/23/fa/beafb91e6e152d2161c4a9cbc472334c87607eb61ad7104b5a7fa8d8d7b1/duckdb-1.5.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3d5db8c0b55e072cf437948ebb5d7e23d7b9d03d905fa5f9145583e65aa447f7", size = 15449411, upload-time = "2026-05-20T11:54:59.089Z" },
- { url = "https://files.pythonhosted.org/packages/50/0a/49b6fe04e2fcd63729eb607dadd44818dde77342a4f5ce086c6c92f1dd4d/duckdb-1.5.3-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ce80aed7a538422129a57eaca9141e3afb51f8bf562b1908b1576c9725b5b22", size = 19333120, upload-time = "2026-05-20T11:55:01.727Z" },
- { url = "https://files.pythonhosted.org/packages/63/4c/0907c3f76adb9dd90e67610b31e0304a35814e65c4c41a354a262c09b885/duckdb-1.5.3-cp313-cp313-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:787df63824f07bf18022dbc3b8ca4b2bfab0ebe616464f55c6e8cd0f59ea762e", size = 21453266, upload-time = "2026-05-20T11:55:04.5Z" },
- { url = "https://files.pythonhosted.org/packages/6d/9c/d2f23a7803ddbbd9413f7572ecf66a15120ed5ced7ce5c73e698c1406b76/duckdb-1.5.3-cp313-cp313-win_amd64.whl", hash = "sha256:bb5bb5dcdd09d62ee60f0ddbbef918e71cce304ffe28428b1131949d39ffaabf", size = 13118640, upload-time = "2026-05-20T11:55:07.389Z" },
- { url = "https://files.pythonhosted.org/packages/27/d5/7ba2316415bcdab6edd765bbbe35c2ca8a3800f2fe695cd70e3cdb997f09/duckdb-1.5.3-cp313-cp313-win_arm64.whl", hash = "sha256:2fa17ecdd5d3db122836cb71bb93601c2106a3be883c17dffddc02fbf3fa7888", size = 13926409, upload-time = "2026-05-20T11:55:10.166Z" },
- { url = "https://files.pythonhosted.org/packages/a5/c2/d4b6f8a5e4d3bc25773be6da76a99d9661ebbf3552c007c460d2dd59dbf8/duckdb-1.5.3-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:4bfa9a4dadf71e83e2c4eaca2f9421c82a54defecc1b0b4c0be95e2389dec4fe", size = 32636685, upload-time = "2026-05-20T11:55:13.158Z" },
- { url = "https://files.pythonhosted.org/packages/42/58/e835c8298979d29db7a62cb5acc29e9b57aeaca7cdde2fcd3ac980f5cb18/duckdb-1.5.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:aea7baf67ad7e1829ac76f67d7dcbd7fb1f57c3eb179d55ac30952df4709ae30", size = 17308134, upload-time = "2026-05-20T11:55:16.194Z" },
- { url = "https://files.pythonhosted.org/packages/c9/46/617b51363f5613418c8b224b3cce16b58e6dde80904566bec232579c1d4e/duckdb-1.5.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0b0b4f088a65d77e1217ce5d7eff889e63fedc44281200d899ff47c84d8ff836", size = 15449891, upload-time = "2026-05-20T11:55:18.687Z" },
- { url = "https://files.pythonhosted.org/packages/b3/72/354146656e8d9ba3853d3a5ee80a481b8c5f70edfc3d5ae80a8c4479c967/duckdb-1.5.3-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fe8d0c1f6a120aa03fa6e0d03897c71a1842e6cf7afd31d181348391f7108fe1", size = 19338499, upload-time = "2026-05-20T11:55:21.34Z" },
- { url = "https://files.pythonhosted.org/packages/56/8f/65fc623b51448f2bfba1a9ec6ab3debb4664c0876c0113a5e782600b53ac/duckdb-1.5.3-cp314-cp314-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d0405eae18ec6e8210a471c97dbfe87a7e4d605274b7fe572a1f276e92158f13", size = 21455828, upload-time = "2026-05-20T11:55:23.847Z" },
- { url = "https://files.pythonhosted.org/packages/2b/db/d0274cbe9f5fe219f77c0bdf900ac77103569e83c102a4225ce04cbc607d/duckdb-1.5.3-cp314-cp314-win_amd64.whl", hash = "sha256:33ae08b3e818d7613d8936744b67718c2062c2f530376895bfd89efb51b81538", size = 13640011, upload-time = "2026-05-20T11:55:26.276Z" },
- { url = "https://files.pythonhosted.org/packages/07/5d/8f1899b8bef291caf953992fcd6c24df9f29387a35645e58c2504a5ca473/duckdb-1.5.3-cp314-cp314-win_arm64.whl", hash = "sha256:746433e49bbc667b4df283153415fbe37e9083e0eff6c3cd6e54de7536869cd4", size = 14411554, upload-time = "2026-05-20T11:55:29.037Z" },
+ { url = "https://files.pythonhosted.org/packages/cc/9c/a528eb09d8be51954c485864bd06753e616939a080cbc3dd4417e8c94a57/duckdb-1.5.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e75a6122c12579a99848517f6f00a4e342aebda3590c30fe9b5cc5f39d5e6afc", size = 32626254, upload-time = "2026-05-20T11:54:53.65Z" },
+ { url = "https://files.pythonhosted.org/packages/ec/3c/1534c0a6db347c05eb7d0f6ecfb7aefbe74cbff398e4892a8fd1903a20e8/duckdb-1.5.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:fd3963c1cb9d9567777f4a898a9dbe388a2fe9724681801b1e7d6d93eecf1b76", size = 17300917, upload-time = "2026-05-20T11:54:56.628Z" },
+ { url = "https://files.pythonhosted.org/packages/23/fa/beafb91e6e152d2161c4a9cbc472334c87607eb61ad7104b5a7fa8d8d7b1/duckdb-1.5.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3d5db8c0b55e072cf437948ebb5d7e23d7b9d03d905fa5f9145583e65aa447f7", size = 15449411, upload-time = "2026-05-20T11:54:59.089Z" },
+ { url = "https://files.pythonhosted.org/packages/50/0a/49b6fe04e2fcd63729eb607dadd44818dde77342a4f5ce086c6c92f1dd4d/duckdb-1.5.3-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ce80aed7a538422129a57eaca9141e3afb51f8bf562b1908b1576c9725b5b22", size = 19333120, upload-time = "2026-05-20T11:55:01.727Z" },
+ { url = "https://files.pythonhosted.org/packages/63/4c/0907c3f76adb9dd90e67610b31e0304a35814e65c4c41a354a262c09b885/duckdb-1.5.3-cp313-cp313-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:787df63824f07bf18022dbc3b8ca4b2bfab0ebe616464f55c6e8cd0f59ea762e", size = 21453266, upload-time = "2026-05-20T11:55:04.5Z" },
+ { url = "https://files.pythonhosted.org/packages/6d/9c/d2f23a7803ddbbd9413f7572ecf66a15120ed5ced7ce5c73e698c1406b76/duckdb-1.5.3-cp313-cp313-win_amd64.whl", hash = "sha256:bb5bb5dcdd09d62ee60f0ddbbef918e71cce304ffe28428b1131949d39ffaabf", size = 13118640, upload-time = "2026-05-20T11:55:07.389Z" },
+ { url = "https://files.pythonhosted.org/packages/27/d5/7ba2316415bcdab6edd765bbbe35c2ca8a3800f2fe695cd70e3cdb997f09/duckdb-1.5.3-cp313-cp313-win_arm64.whl", hash = "sha256:2fa17ecdd5d3db122836cb71bb93601c2106a3be883c17dffddc02fbf3fa7888", size = 13926409, upload-time = "2026-05-20T11:55:10.166Z" },
+ { url = "https://files.pythonhosted.org/packages/a5/c2/d4b6f8a5e4d3bc25773be6da76a99d9661ebbf3552c007c460d2dd59dbf8/duckdb-1.5.3-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:4bfa9a4dadf71e83e2c4eaca2f9421c82a54defecc1b0b4c0be95e2389dec4fe", size = 32636685, upload-time = "2026-05-20T11:55:13.158Z" },
+ { url = "https://files.pythonhosted.org/packages/42/58/e835c8298979d29db7a62cb5acc29e9b57aeaca7cdde2fcd3ac980f5cb18/duckdb-1.5.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:aea7baf67ad7e1829ac76f67d7dcbd7fb1f57c3eb179d55ac30952df4709ae30", size = 17308134, upload-time = "2026-05-20T11:55:16.194Z" },
+ { url = "https://files.pythonhosted.org/packages/c9/46/617b51363f5613418c8b224b3cce16b58e6dde80904566bec232579c1d4e/duckdb-1.5.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0b0b4f088a65d77e1217ce5d7eff889e63fedc44281200d899ff47c84d8ff836", size = 15449891, upload-time = "2026-05-20T11:55:18.687Z" },
+ { url = "https://files.pythonhosted.org/packages/b3/72/354146656e8d9ba3853d3a5ee80a481b8c5f70edfc3d5ae80a8c4479c967/duckdb-1.5.3-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fe8d0c1f6a120aa03fa6e0d03897c71a1842e6cf7afd31d181348391f7108fe1", size = 19338499, upload-time = "2026-05-20T11:55:21.34Z" },
+ { url = "https://files.pythonhosted.org/packages/56/8f/65fc623b51448f2bfba1a9ec6ab3debb4664c0876c0113a5e782600b53ac/duckdb-1.5.3-cp314-cp314-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d0405eae18ec6e8210a471c97dbfe87a7e4d605274b7fe572a1f276e92158f13", size = 21455828, upload-time = "2026-05-20T11:55:23.847Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/db/d0274cbe9f5fe219f77c0bdf900ac77103569e83c102a4225ce04cbc607d/duckdb-1.5.3-cp314-cp314-win_amd64.whl", hash = "sha256:33ae08b3e818d7613d8936744b67718c2062c2f530376895bfd89efb51b81538", size = 13640011, upload-time = "2026-05-20T11:55:26.276Z" },
+ { url = "https://files.pythonhosted.org/packages/07/5d/8f1899b8bef291caf953992fcd6c24df9f29387a35645e58c2504a5ca473/duckdb-1.5.3-cp314-cp314-win_arm64.whl", hash = "sha256:746433e49bbc667b4df283153415fbe37e9083e0eff6c3cd6e54de7536869cd4", size = 14411554, upload-time = "2026-05-20T11:55:29.037Z" },
]
[[package]]
@@ -268,15 +268,15 @@ name = "fastapi"
version = "0.136.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
- { name = "annotated-doc" },
- { name = "pydantic" },
- { name = "starlette" },
- { name = "typing-extensions" },
- { name = "typing-inspection" },
+ { name = "annotated-doc" },
+ { name = "pydantic" },
+ { name = "starlette" },
+ { name = "typing-extensions" },
+ { name = "typing-inspection" },
]
sdist = { url = "https://files.pythonhosted.org/packages/81/2d/ff8d91d7b564d464629a0fd50a4489c97fcb836ac230bf3a7269232a9b1f/fastapi-0.136.3.tar.gz", hash = "sha256:e487fae93ad408e6f47641ee4dfe389864fd7bec92e547ea8498fc13f43e83ab", size = 396410, upload-time = "2026-05-23T18:53:15.192Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/e0/82/45359b62a067409bd929ae8a56b8ed13e5a8c8a61194b3c236920999ab83/fastapi-0.136.3-py3-none-any.whl", hash = "sha256:3d2a69bdf04b7e9f3afa292c3bc7a98816bbfafa10bc9b45f3f3700d2f761620", size = 117481, upload-time = "2026-05-23T18:53:16.924Z" },
+ { url = "https://files.pythonhosted.org/packages/e0/82/45359b62a067409bd929ae8a56b8ed13e5a8c8a61194b3c236920999ab83/fastapi-0.136.3-py3-none-any.whl", hash = "sha256:3d2a69bdf04b7e9f3afa292c3bc7a98816bbfafa10bc9b45f3f3700d2f761620", size = 117481, upload-time = "2026-05-23T18:53:16.924Z" },
]
[[package]]
@@ -285,7 +285,7 @@ version = "3.29.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/b5/fe/997687a931ab51049acce6fa1f23e8f01216374ea81374ddee763c493db5/filelock-3.29.0.tar.gz", hash = "sha256:69974355e960702e789734cb4871f884ea6fe50bd8404051a3530bc07809cf90", size = 57571, upload-time = "2026-04-19T15:39:10.068Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/81/47/dd9a212ef6e343a6857485ffe25bba537304f1913bdbed446a23f7f592e1/filelock-3.29.0-py3-none-any.whl", hash = "sha256:96f5f6344709aa1572bbf631c640e4ebeeb519e08da902c39a001882f30ac258", size = 39812, upload-time = "2026-04-19T15:39:08.752Z" },
+ { url = "https://files.pythonhosted.org/packages/81/47/dd9a212ef6e343a6857485ffe25bba537304f1913bdbed446a23f7f592e1/filelock-3.29.0-py3-none-any.whl", hash = "sha256:96f5f6344709aa1572bbf631c640e4ebeeb519e08da902c39a001882f30ac258", size = 39812, upload-time = "2026-04-19T15:39:08.752Z" },
]
[[package]]
@@ -294,7 +294,7 @@ version = "2026.4.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d5/8d/1c51c094345df128ca4a990d633fe1a0ff28726c9e6b3c41ba65087bba1d/fsspec-2026.4.0.tar.gz", hash = "sha256:301d8ac70ae90ef3ad05dcf94d6c3754a097f9b5fe4667d2787aa359ec7df7e4", size = 312760, upload-time = "2026-04-29T20:42:38.635Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/d5/0c/043d5e551459da400957a1395e0febbf771446ff34291afcbe3d8be2a279/fsspec-2026.4.0-py3-none-any.whl", hash = "sha256:11ef7bb35dab8a394fde6e608221d5cf3e8499401c249bebaeaad760a1a8dec2", size = 203402, upload-time = "2026-04-29T20:42:36.842Z" },
+ { url = "https://files.pythonhosted.org/packages/d5/0c/043d5e551459da400957a1395e0febbf771446ff34291afcbe3d8be2a279/fsspec-2026.4.0-py3-none-any.whl", hash = "sha256:11ef7bb35dab8a394fde6e608221d5cf3e8499401c249bebaeaad760a1a8dec2", size = 203402, upload-time = "2026-04-29T20:42:36.842Z" },
]
[[package]]
@@ -303,7 +303,7 @@ version = "0.16.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" },
+ { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" },
]
[[package]]
@@ -312,30 +312,30 @@ version = "1.5.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/74/d8/5c06fc76461418326a7decf8367480c35be11a41fd938633929c60a9ec6b/hf_xet-1.5.0.tar.gz", hash = "sha256:e0fb0a34d9f406eed88233e829a67ec016bec5af19e480eac65a233ea289a948", size = 837196, upload-time = "2026-05-06T06:18:15.583Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/68/9b/6912c99070915a4f28119e3c5b52a9abd1eec0ad5cb293b8c967a0c6f5a2/hf_xet-1.5.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:7d70fe2ce97b9db73b9c9b9c81fe3693640aec83416a966c446afea54acfae3c", size = 4023383, upload-time = "2026-05-06T06:17:53.947Z" },
- { url = "https://files.pythonhosted.org/packages/0f/6d/9563cfde59b5d8128a9c7ec972a087f4c782e4f7bac5a85234edfd5d5e49/hf_xet-1.5.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:73a0dae8c71de3b0633a45c73f4a4a5ed09e94b43441d82981a781d4f12baa42", size = 3792751, upload-time = "2026-05-06T06:17:51.791Z" },
- { url = "https://files.pythonhosted.org/packages/07/a5/ed5a0cf35b49a0571af5a8f53416dad1877a718c021c9937c3a53cb45781/hf_xet-1.5.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a60290ec57e9b71767fba7c3645ddafdd0759974b540441510c629c6db6db24a", size = 4456058, upload-time = "2026-05-06T06:17:40.735Z" },
- { url = "https://files.pythonhosted.org/packages/60/fb/3ae8bf2a7a37a4197d0195d7247fd25b3952e15cb8a599e285dfaa6f52b3/hf_xet-1.5.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:e5de0f6deada0dada870bb376a11bcd1f08abf3a968a6d118f33e72d1b1eb480", size = 4250783, upload-time = "2026-05-06T06:17:38.412Z" },
- { url = "https://files.pythonhosted.org/packages/a2/9b/8bae40d4d91525085137196e84eb0ed49cf65b5e96e5c3ecdadd8bd0fac2/hf_xet-1.5.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c799d49f1a5544a0ef7591c0ee75e0d6b93d6f56dc7a4979f59f7518d2872216", size = 4445594, upload-time = "2026-05-06T06:18:04.219Z" },
- { url = "https://files.pythonhosted.org/packages/13/59/c74efbbd4e8728172b2cc72a2bc014d2947a4b7bdced932fbd3f5da1a4e5/hf_xet-1.5.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2baea1b0b989e5c152fe81425f7745ddc8901280ba3d97c98d8cdece7b706c60", size = 4663995, upload-time = "2026-05-06T06:18:06.1Z" },
- { url = "https://files.pythonhosted.org/packages/73/32/8e1e0410af64cda9b139d1dcebdc993a8ff9c8c7c0e2696ae356d75ccc0d/hf_xet-1.5.0-cp313-cp313t-win_amd64.whl", hash = "sha256:526345b3ed45f374f6317349df489167606736c876241ba984105afe7fd4839d", size = 3966608, upload-time = "2026-05-06T06:18:19.74Z" },
- { url = "https://files.pythonhosted.org/packages/fc/34/a8febc8f4edbea8b3e21b02ebc8b628679b84ba7e45cde624a7736b51500/hf_xet-1.5.0-cp313-cp313t-win_arm64.whl", hash = "sha256:786d28e2eb8315d5035544b9d137b4a842d600c434bb91bf7d0d953cce906ad4", size = 3796946, upload-time = "2026-05-06T06:18:17.568Z" },
- { url = "https://files.pythonhosted.org/packages/2a/20/8fc8996afe5815fa1a6be8e9e5c02f24500f409d599e905800d498a4e14d/hf_xet-1.5.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:872d5601e6deea30d15865ede55d29eac6daf5a534ab417b99b6ef6b076dd96c", size = 4023495, upload-time = "2026-05-06T06:18:01.94Z" },
- { url = "https://files.pythonhosted.org/packages/32/6a/93d84463c00cecb561a7508aa6303e35ee2894294eac14245526924415fe/hf_xet-1.5.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9929561f5abf4581c8ea79587881dfef6b8abb2a0d8a51915936fc2a614f4e73", size = 3792731, upload-time = "2026-05-06T06:18:00.021Z" },
- { url = "https://files.pythonhosted.org/packages/9d/5a/8ec8e0c863b382d00b3c2e2af6ded6b06371be617144a625903a6d562f4b/hf_xet-1.5.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f7b7bbae318e583a86fb21e5a4a175d6721d628a2874f4bd022d0e660c32a682", size = 4456738, upload-time = "2026-05-06T06:17:49.574Z" },
- { url = "https://files.pythonhosted.org/packages/c5/ca/f7effa1a67717da2bcc6b6c28f71c6ca648c77acaec4e2c32f40cbe16d85/hf_xet-1.5.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:cf7b2dc6f31a4ea754bb50f74cde482dcf5d366d184076d8530b9872787f3761", size = 4251622, upload-time = "2026-05-06T06:17:47.096Z" },
- { url = "https://files.pythonhosted.org/packages/65/f2/19247dba3e231cf77dec59ddfb878f00057635ff773d099c9b59d37812c3/hf_xet-1.5.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8dbcbab554c9ef158ef2c991545c3e970ddd8cc7acdcd0a78c5a41095dab4ded", size = 4445667, upload-time = "2026-05-06T06:18:11.983Z" },
- { url = "https://files.pythonhosted.org/packages/7f/64/6f116801a3bcfb6f59f5c251f48cadc47ea54026441c4a385079286a94fa/hf_xet-1.5.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5906bf7718d3636dc13402914736abe723492cb730f744834f5f5b67d3a12702", size = 4664619, upload-time = "2026-05-06T06:18:13.771Z" },
- { url = "https://files.pythonhosted.org/packages/5c/e8/069542d37946ed08669b127e1496fa99e78196d71de8d41eda5e9f1b7a58/hf_xet-1.5.0-cp314-cp314t-win_amd64.whl", hash = "sha256:5f3dc2248fc01cc0a00cd392ab497f1ca373fcbc7e3f2da1f452480b384e839e", size = 3966802, upload-time = "2026-05-06T06:18:28.162Z" },
- { url = "https://files.pythonhosted.org/packages/f9/91/fc6fdec27b14d04e88c386ac0a0129732b53fa23f7c4a78f4b83a039c567/hf_xet-1.5.0-cp314-cp314t-win_arm64.whl", hash = "sha256:b285cea1b5bab46b758772716ba8d6854a1a0310fed1c249d678a8b38601e5a0", size = 3797168, upload-time = "2026-05-06T06:18:26.287Z" },
- { url = "https://files.pythonhosted.org/packages/3d/fb/69ff198a82cae7eb1a69fb84d93b3a3e4816564d76817fe541ddc96874eb/hf_xet-1.5.0-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:dad0dc84e941b8ba3c860659fe1fdc35c049d47cce293f003287757e971a8f56", size = 4030814, upload-time = "2026-05-06T06:17:57.933Z" },
- { url = "https://files.pythonhosted.org/packages/9b/ff/edcc2b40162bef3ff78e14ab637e5f3b89243d6aee72f5949d3bb6a5af83/hf_xet-1.5.0-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:fd6e5a9b0fdac4ed03ed45ef79254a655b1aaab514a02202617fbf643f5fdf7a", size = 3798444, upload-time = "2026-05-06T06:17:55.79Z" },
- { url = "https://files.pythonhosted.org/packages/49/4d/103f76b04310e5e57656696cc184690d20c466af0bca3ca88f8c8ea5d4f3/hf_xet-1.5.0-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3531b1823a0e6d77d80f9ed15ca0e00f0d115094f8ac033d5cae88f4564cc949", size = 4465986, upload-time = "2026-05-06T06:17:44.886Z" },
- { url = "https://files.pythonhosted.org/packages/c4/a2/546f47f464737b3edbab6f8ddb57f2599b93d2cbb66f06abb475ccb48651/hf_xet-1.5.0-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:9a0ee58cd18d5ea799f7ed11290bbccbe56bdd8b1d97ca74b9cc49a3945d7a3b", size = 4259865, upload-time = "2026-05-06T06:17:42.639Z" },
- { url = "https://files.pythonhosted.org/packages/95/7f/1be593c1f28613be2e196473481cd81bfc5910795e30a34e8f744f6cac4f/hf_xet-1.5.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1e60df5a42e9bed8628b6416af2cba4cba57ae9f02de226a06b020d98e1aab18", size = 4459835, upload-time = "2026-05-06T06:18:08.026Z" },
- { url = "https://files.pythonhosted.org/packages/aa/b2/703569fc881f3284487e68cda7b42179978480da3c438042a6bbbb4a671c/hf_xet-1.5.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:4b35549ce62601b84da4ff9b24d970032ace3d4430f52d91bcbb26c901d6c690", size = 4672414, upload-time = "2026-05-06T06:18:09.864Z" },
- { url = "https://files.pythonhosted.org/packages/af/37/1b6def445c567286b50aa3b33828158e135b1be44938dde59f11382a500c/hf_xet-1.5.0-cp37-abi3-win_amd64.whl", hash = "sha256:2806c7c17b4d23f8d88f7c4814f838c3b6150773fe339c20af23e1cfaf2797e4", size = 3977238, upload-time = "2026-05-06T06:18:23.621Z" },
- { url = "https://files.pythonhosted.org/packages/62/94/3b66b148778ee100dcfd69c2ca22b57b41b44d3063ceec934f209e9184ce/hf_xet-1.5.0-cp37-abi3-win_arm64.whl", hash = "sha256:b6c9df403040248c76d808d3e047d64db2d923bae593eb244c41e425cf6cd7be", size = 3806916, upload-time = "2026-05-06T06:18:21.7Z" },
+ { url = "https://files.pythonhosted.org/packages/68/9b/6912c99070915a4f28119e3c5b52a9abd1eec0ad5cb293b8c967a0c6f5a2/hf_xet-1.5.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:7d70fe2ce97b9db73b9c9b9c81fe3693640aec83416a966c446afea54acfae3c", size = 4023383, upload-time = "2026-05-06T06:17:53.947Z" },
+ { url = "https://files.pythonhosted.org/packages/0f/6d/9563cfde59b5d8128a9c7ec972a087f4c782e4f7bac5a85234edfd5d5e49/hf_xet-1.5.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:73a0dae8c71de3b0633a45c73f4a4a5ed09e94b43441d82981a781d4f12baa42", size = 3792751, upload-time = "2026-05-06T06:17:51.791Z" },
+ { url = "https://files.pythonhosted.org/packages/07/a5/ed5a0cf35b49a0571af5a8f53416dad1877a718c021c9937c3a53cb45781/hf_xet-1.5.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a60290ec57e9b71767fba7c3645ddafdd0759974b540441510c629c6db6db24a", size = 4456058, upload-time = "2026-05-06T06:17:40.735Z" },
+ { url = "https://files.pythonhosted.org/packages/60/fb/3ae8bf2a7a37a4197d0195d7247fd25b3952e15cb8a599e285dfaa6f52b3/hf_xet-1.5.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:e5de0f6deada0dada870bb376a11bcd1f08abf3a968a6d118f33e72d1b1eb480", size = 4250783, upload-time = "2026-05-06T06:17:38.412Z" },
+ { url = "https://files.pythonhosted.org/packages/a2/9b/8bae40d4d91525085137196e84eb0ed49cf65b5e96e5c3ecdadd8bd0fac2/hf_xet-1.5.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c799d49f1a5544a0ef7591c0ee75e0d6b93d6f56dc7a4979f59f7518d2872216", size = 4445594, upload-time = "2026-05-06T06:18:04.219Z" },
+ { url = "https://files.pythonhosted.org/packages/13/59/c74efbbd4e8728172b2cc72a2bc014d2947a4b7bdced932fbd3f5da1a4e5/hf_xet-1.5.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2baea1b0b989e5c152fe81425f7745ddc8901280ba3d97c98d8cdece7b706c60", size = 4663995, upload-time = "2026-05-06T06:18:06.1Z" },
+ { url = "https://files.pythonhosted.org/packages/73/32/8e1e0410af64cda9b139d1dcebdc993a8ff9c8c7c0e2696ae356d75ccc0d/hf_xet-1.5.0-cp313-cp313t-win_amd64.whl", hash = "sha256:526345b3ed45f374f6317349df489167606736c876241ba984105afe7fd4839d", size = 3966608, upload-time = "2026-05-06T06:18:19.74Z" },
+ { url = "https://files.pythonhosted.org/packages/fc/34/a8febc8f4edbea8b3e21b02ebc8b628679b84ba7e45cde624a7736b51500/hf_xet-1.5.0-cp313-cp313t-win_arm64.whl", hash = "sha256:786d28e2eb8315d5035544b9d137b4a842d600c434bb91bf7d0d953cce906ad4", size = 3796946, upload-time = "2026-05-06T06:18:17.568Z" },
+ { url = "https://files.pythonhosted.org/packages/2a/20/8fc8996afe5815fa1a6be8e9e5c02f24500f409d599e905800d498a4e14d/hf_xet-1.5.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:872d5601e6deea30d15865ede55d29eac6daf5a534ab417b99b6ef6b076dd96c", size = 4023495, upload-time = "2026-05-06T06:18:01.94Z" },
+ { url = "https://files.pythonhosted.org/packages/32/6a/93d84463c00cecb561a7508aa6303e35ee2894294eac14245526924415fe/hf_xet-1.5.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9929561f5abf4581c8ea79587881dfef6b8abb2a0d8a51915936fc2a614f4e73", size = 3792731, upload-time = "2026-05-06T06:18:00.021Z" },
+ { url = "https://files.pythonhosted.org/packages/9d/5a/8ec8e0c863b382d00b3c2e2af6ded6b06371be617144a625903a6d562f4b/hf_xet-1.5.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f7b7bbae318e583a86fb21e5a4a175d6721d628a2874f4bd022d0e660c32a682", size = 4456738, upload-time = "2026-05-06T06:17:49.574Z" },
+ { url = "https://files.pythonhosted.org/packages/c5/ca/f7effa1a67717da2bcc6b6c28f71c6ca648c77acaec4e2c32f40cbe16d85/hf_xet-1.5.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:cf7b2dc6f31a4ea754bb50f74cde482dcf5d366d184076d8530b9872787f3761", size = 4251622, upload-time = "2026-05-06T06:17:47.096Z" },
+ { url = "https://files.pythonhosted.org/packages/65/f2/19247dba3e231cf77dec59ddfb878f00057635ff773d099c9b59d37812c3/hf_xet-1.5.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8dbcbab554c9ef158ef2c991545c3e970ddd8cc7acdcd0a78c5a41095dab4ded", size = 4445667, upload-time = "2026-05-06T06:18:11.983Z" },
+ { url = "https://files.pythonhosted.org/packages/7f/64/6f116801a3bcfb6f59f5c251f48cadc47ea54026441c4a385079286a94fa/hf_xet-1.5.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5906bf7718d3636dc13402914736abe723492cb730f744834f5f5b67d3a12702", size = 4664619, upload-time = "2026-05-06T06:18:13.771Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/e8/069542d37946ed08669b127e1496fa99e78196d71de8d41eda5e9f1b7a58/hf_xet-1.5.0-cp314-cp314t-win_amd64.whl", hash = "sha256:5f3dc2248fc01cc0a00cd392ab497f1ca373fcbc7e3f2da1f452480b384e839e", size = 3966802, upload-time = "2026-05-06T06:18:28.162Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/91/fc6fdec27b14d04e88c386ac0a0129732b53fa23f7c4a78f4b83a039c567/hf_xet-1.5.0-cp314-cp314t-win_arm64.whl", hash = "sha256:b285cea1b5bab46b758772716ba8d6854a1a0310fed1c249d678a8b38601e5a0", size = 3797168, upload-time = "2026-05-06T06:18:26.287Z" },
+ { url = "https://files.pythonhosted.org/packages/3d/fb/69ff198a82cae7eb1a69fb84d93b3a3e4816564d76817fe541ddc96874eb/hf_xet-1.5.0-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:dad0dc84e941b8ba3c860659fe1fdc35c049d47cce293f003287757e971a8f56", size = 4030814, upload-time = "2026-05-06T06:17:57.933Z" },
+ { url = "https://files.pythonhosted.org/packages/9b/ff/edcc2b40162bef3ff78e14ab637e5f3b89243d6aee72f5949d3bb6a5af83/hf_xet-1.5.0-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:fd6e5a9b0fdac4ed03ed45ef79254a655b1aaab514a02202617fbf643f5fdf7a", size = 3798444, upload-time = "2026-05-06T06:17:55.79Z" },
+ { url = "https://files.pythonhosted.org/packages/49/4d/103f76b04310e5e57656696cc184690d20c466af0bca3ca88f8c8ea5d4f3/hf_xet-1.5.0-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3531b1823a0e6d77d80f9ed15ca0e00f0d115094f8ac033d5cae88f4564cc949", size = 4465986, upload-time = "2026-05-06T06:17:44.886Z" },
+ { url = "https://files.pythonhosted.org/packages/c4/a2/546f47f464737b3edbab6f8ddb57f2599b93d2cbb66f06abb475ccb48651/hf_xet-1.5.0-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:9a0ee58cd18d5ea799f7ed11290bbccbe56bdd8b1d97ca74b9cc49a3945d7a3b", size = 4259865, upload-time = "2026-05-06T06:17:42.639Z" },
+ { url = "https://files.pythonhosted.org/packages/95/7f/1be593c1f28613be2e196473481cd81bfc5910795e30a34e8f744f6cac4f/hf_xet-1.5.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1e60df5a42e9bed8628b6416af2cba4cba57ae9f02de226a06b020d98e1aab18", size = 4459835, upload-time = "2026-05-06T06:18:08.026Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/b2/703569fc881f3284487e68cda7b42179978480da3c438042a6bbbb4a671c/hf_xet-1.5.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:4b35549ce62601b84da4ff9b24d970032ace3d4430f52d91bcbb26c901d6c690", size = 4672414, upload-time = "2026-05-06T06:18:09.864Z" },
+ { url = "https://files.pythonhosted.org/packages/af/37/1b6def445c567286b50aa3b33828158e135b1be44938dde59f11382a500c/hf_xet-1.5.0-cp37-abi3-win_amd64.whl", hash = "sha256:2806c7c17b4d23f8d88f7c4814f838c3b6150773fe339c20af23e1cfaf2797e4", size = 3977238, upload-time = "2026-05-06T06:18:23.621Z" },
+ { url = "https://files.pythonhosted.org/packages/62/94/3b66b148778ee100dcfd69c2ca22b57b41b44d3063ceec934f209e9184ce/hf_xet-1.5.0-cp37-abi3-win_arm64.whl", hash = "sha256:b6c9df403040248c76d808d3e047d64db2d923bae593eb244c41e425cf6cd7be", size = 3806916, upload-time = "2026-05-06T06:18:21.7Z" },
]
[[package]]
@@ -343,12 +343,12 @@ name = "httpcore"
version = "1.0.9"
source = { registry = "https://pypi.org/simple" }
dependencies = [
- { name = "certifi" },
- { name = "h11" },
+ { name = "certifi" },
+ { name = "h11" },
]
sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" },
+ { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" },
]
[[package]]
@@ -357,27 +357,27 @@ version = "0.8.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/43/e5/d471fcb0e14523fe1c3f4ba58ca52480e7bd70ad7109a3846bc75892f7fb/httptools-0.8.0.tar.gz", hash = "sha256:6b2a32f18d97e16e90827d7a819ffa8dbd8cc245fc4e1fa9d1095b54ef4bd999", size = 271342, upload-time = "2026-05-25T22:17:48.841Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/5e/e5/8cfcabc5546e8022f168be28bcdaa128a240a0befdd03b59d558b4f18bd6/httptools-0.8.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:614ceea8ea606848bece2338ac03b3ce5324bcb4be8dc7d377ed708012fa4db8", size = 205148, upload-time = "2026-05-25T22:17:16.333Z" },
- { url = "https://files.pythonhosted.org/packages/2a/0e/0fb14848c19a686c8062ff9067c1a48793e3224b47bc5b201535b6036fce/httptools-0.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2d689918c15a013c65ef52d9fd495d766893ab831a2c8d89f2ac5940a5df847c", size = 111368, upload-time = "2026-05-25T22:17:17.586Z" },
- { url = "https://files.pythonhosted.org/packages/2e/1b/46f1cecf06b9bbde8e4b8c88034ac7908989e5ff7a3a388ef38392949c1f/httptools-0.8.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:eb3028cca2fc0a6d720e52ef61d8ebb62fcbfeb1de56874546d858d3f25a26b7", size = 486447, upload-time = "2026-05-25T22:17:18.564Z" },
- { url = "https://files.pythonhosted.org/packages/77/00/258bfc0837221f81d9725c45f9b948a6a6b2994a147a4fb66e85100c668f/httptools-0.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:88bdd940f2b5d487b4d032c6afa5489a7dc4694410d43de3c38c4fb3af0dc45d", size = 482448, upload-time = "2026-05-25T22:17:19.912Z" },
- { url = "https://files.pythonhosted.org/packages/04/ab/d1cef3b5523f4d272a70f42a776c3169a2dddfe3a54de4b2ce4a36341528/httptools-0.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6a43c9dd399758ccc0531acb0a3c4a6c299ee893ee9400e9c893b7bdcfae0681", size = 464460, upload-time = "2026-05-25T22:17:20.882Z" },
- { url = "https://files.pythonhosted.org/packages/ce/48/5d1d072442277bb2b3434e0e60690b8e8c23840ef7de8b6ea54040a536d3/httptools-0.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0770728beb05094c809b98e814edff5fef69d26ad7d21185f2f6d5884a0ba683", size = 471312, upload-time = "2026-05-25T22:17:22.085Z" },
- { url = "https://files.pythonhosted.org/packages/0d/66/b96623b27e51a68199ef4efdda0613cced9233fe3062ac74e50749c5ad37/httptools-0.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:7685df791fad561384bfb139e77fde27a1ffd93134e016f95a0db424ffbf77b1", size = 90117, upload-time = "2026-05-25T22:17:23.074Z" },
- { url = "https://files.pythonhosted.org/packages/1a/12/fa3fbf5f9517b273edea2dc982aa82a8c634091e67c590792b729017bc6f/httptools-0.8.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:de242a49b5d18e0a8776e654e9f6bf6d89f3875a5c35b425a0e7ce940feb3fd6", size = 206183, upload-time = "2026-05-25T22:17:24.004Z" },
- { url = "https://files.pythonhosted.org/packages/30/fc/5e7c4cb443370f2090a3aba0453a07384d29ff66b7435bb90e77e1037599/httptools-0.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:159e9ab5f701ccd42e555a12f1ad8ff69702910fc1c996cf2bb66e5fcb7a231b", size = 112079, upload-time = "2026-05-25T22:17:25.216Z" },
- { url = "https://files.pythonhosted.org/packages/ba/53/771bd891eb0f236f32145d6a1775777ec85745f3cc983a1f23d1a3b8ddfe/httptools-0.8.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c4a9f1707e4823d54dfec6c33fa3697d302aed536ed352a7ebb5a061ddb869d0", size = 481596, upload-time = "2026-05-25T22:17:26.186Z" },
- { url = "https://files.pythonhosted.org/packages/62/42/94e15bc68ce3d423243c45d7f1b0c7561f13844f97dc52ae23182fb65628/httptools-0.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d76ad7b951387e3632c8716a9bb03ac5b45c5f16119aa409db0459520887944e", size = 480865, upload-time = "2026-05-25T22:17:27.542Z" },
- { url = "https://files.pythonhosted.org/packages/1c/7c/fe2980fc03723272e30f135b62360b075f513dfe7cc73aef36c7f04012bd/httptools-0.8.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a3b7387147361c3fd47a0bde763c5c91b5b4cd4dc9989b8ece84ff436c99843b", size = 463189, upload-time = "2026-05-25T22:17:28.546Z" },
- { url = "https://files.pythonhosted.org/packages/15/1b/47fc5fff68acd1bfa20b4734059c9a06cadb88119dcd5258b5b0d21d91c8/httptools-0.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f256d6ce930c52ca1cb2a960b7da03548c454e7d28b06059ad41bfe789036ce0", size = 466610, upload-time = "2026-05-25T22:17:29.816Z" },
- { url = "https://files.pythonhosted.org/packages/60/bd/07b13c93ffd9bec9546e0d43f8e19378dd696dbd278511406bc07371ef1f/httptools-0.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:19d1ee275bb59ba2643ba9a3a1e51cc0c788caf2b8df506368e03f56fdd08527", size = 92705, upload-time = "2026-05-25T22:17:31.133Z" },
- { url = "https://files.pythonhosted.org/packages/fd/c4/121648f68ce066d7bd762d6b6d97e620847642d38d54f3d90ff11d947629/httptools-0.8.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:de1ed58a974e75d56560acc7e7fed01a454994429456f65209789992e41f2568", size = 215023, upload-time = "2026-05-25T22:17:32.401Z" },
- { url = "https://files.pythonhosted.org/packages/b9/b0/312a062ae741ae3e8baa8c8bf20be81b2e67337b259ab4349bebc7b6142e/httptools-0.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e93c227b595c6926c1acee96891dd9da4be338cfbe82e5cd3bb9d8dd7dc4ac0b", size = 117405, upload-time = "2026-05-25T22:17:33.742Z" },
- { url = "https://files.pythonhosted.org/packages/fc/37/fccd705f795386bb05bf413012fecff2a33e5aa8c2f069096de3e9fd8702/httptools-0.8.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2a021c3a8e65cc125390d72f59b968afca3bdcaff25bd67965e0a055a14946ca", size = 558497, upload-time = "2026-05-25T22:17:34.732Z" },
- { url = "https://files.pythonhosted.org/packages/bd/39/f172e8003576de35f5ba77ff417cf0e34429d35dc014deef15afa337a72c/httptools-0.8.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:48774d39cbb70e2b1f71f88852a3087ae1d3a1eb80482bb48c13067ab080c14f", size = 571585, upload-time = "2026-05-25T22:17:35.813Z" },
- { url = "https://files.pythonhosted.org/packages/3e/b9/f5564760af99f3dbbf3f9104dc00e5da27e96cf433c6bdcf77617f70bf3f/httptools-0.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:88eead8ec8680a9f146c655bc88445a325bd7921cfd8194c7337e9467282427d", size = 543297, upload-time = "2026-05-25T22:17:37.08Z" },
- { url = "https://files.pythonhosted.org/packages/99/67/8d9f2c313618e161b82f3873188e7196126da1d6e29688df40eb3997c77a/httptools-0.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:2c032fa028f46871ec7e1fc59fc15e8023eab3e6bbe6ece786a1611719a5d081", size = 539535, upload-time = "2026-05-25T22:17:38.032Z" },
- { url = "https://files.pythonhosted.org/packages/48/63/b906c01e53f50d432c0defe43ce52764a111dc1bdd028bafbeb54dcfd008/httptools-0.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:384c17174464c8e873398b7af24f0b1f44d992c820328413951a625323155d77", size = 108209, upload-time = "2026-05-25T22:17:39.473Z" },
+ { url = "https://files.pythonhosted.org/packages/5e/e5/8cfcabc5546e8022f168be28bcdaa128a240a0befdd03b59d558b4f18bd6/httptools-0.8.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:614ceea8ea606848bece2338ac03b3ce5324bcb4be8dc7d377ed708012fa4db8", size = 205148, upload-time = "2026-05-25T22:17:16.333Z" },
+ { url = "https://files.pythonhosted.org/packages/2a/0e/0fb14848c19a686c8062ff9067c1a48793e3224b47bc5b201535b6036fce/httptools-0.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2d689918c15a013c65ef52d9fd495d766893ab831a2c8d89f2ac5940a5df847c", size = 111368, upload-time = "2026-05-25T22:17:17.586Z" },
+ { url = "https://files.pythonhosted.org/packages/2e/1b/46f1cecf06b9bbde8e4b8c88034ac7908989e5ff7a3a388ef38392949c1f/httptools-0.8.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:eb3028cca2fc0a6d720e52ef61d8ebb62fcbfeb1de56874546d858d3f25a26b7", size = 486447, upload-time = "2026-05-25T22:17:18.564Z" },
+ { url = "https://files.pythonhosted.org/packages/77/00/258bfc0837221f81d9725c45f9b948a6a6b2994a147a4fb66e85100c668f/httptools-0.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:88bdd940f2b5d487b4d032c6afa5489a7dc4694410d43de3c38c4fb3af0dc45d", size = 482448, upload-time = "2026-05-25T22:17:19.912Z" },
+ { url = "https://files.pythonhosted.org/packages/04/ab/d1cef3b5523f4d272a70f42a776c3169a2dddfe3a54de4b2ce4a36341528/httptools-0.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6a43c9dd399758ccc0531acb0a3c4a6c299ee893ee9400e9c893b7bdcfae0681", size = 464460, upload-time = "2026-05-25T22:17:20.882Z" },
+ { url = "https://files.pythonhosted.org/packages/ce/48/5d1d072442277bb2b3434e0e60690b8e8c23840ef7de8b6ea54040a536d3/httptools-0.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0770728beb05094c809b98e814edff5fef69d26ad7d21185f2f6d5884a0ba683", size = 471312, upload-time = "2026-05-25T22:17:22.085Z" },
+ { url = "https://files.pythonhosted.org/packages/0d/66/b96623b27e51a68199ef4efdda0613cced9233fe3062ac74e50749c5ad37/httptools-0.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:7685df791fad561384bfb139e77fde27a1ffd93134e016f95a0db424ffbf77b1", size = 90117, upload-time = "2026-05-25T22:17:23.074Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/12/fa3fbf5f9517b273edea2dc982aa82a8c634091e67c590792b729017bc6f/httptools-0.8.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:de242a49b5d18e0a8776e654e9f6bf6d89f3875a5c35b425a0e7ce940feb3fd6", size = 206183, upload-time = "2026-05-25T22:17:24.004Z" },
+ { url = "https://files.pythonhosted.org/packages/30/fc/5e7c4cb443370f2090a3aba0453a07384d29ff66b7435bb90e77e1037599/httptools-0.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:159e9ab5f701ccd42e555a12f1ad8ff69702910fc1c996cf2bb66e5fcb7a231b", size = 112079, upload-time = "2026-05-25T22:17:25.216Z" },
+ { url = "https://files.pythonhosted.org/packages/ba/53/771bd891eb0f236f32145d6a1775777ec85745f3cc983a1f23d1a3b8ddfe/httptools-0.8.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c4a9f1707e4823d54dfec6c33fa3697d302aed536ed352a7ebb5a061ddb869d0", size = 481596, upload-time = "2026-05-25T22:17:26.186Z" },
+ { url = "https://files.pythonhosted.org/packages/62/42/94e15bc68ce3d423243c45d7f1b0c7561f13844f97dc52ae23182fb65628/httptools-0.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d76ad7b951387e3632c8716a9bb03ac5b45c5f16119aa409db0459520887944e", size = 480865, upload-time = "2026-05-25T22:17:27.542Z" },
+ { url = "https://files.pythonhosted.org/packages/1c/7c/fe2980fc03723272e30f135b62360b075f513dfe7cc73aef36c7f04012bd/httptools-0.8.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a3b7387147361c3fd47a0bde763c5c91b5b4cd4dc9989b8ece84ff436c99843b", size = 463189, upload-time = "2026-05-25T22:17:28.546Z" },
+ { url = "https://files.pythonhosted.org/packages/15/1b/47fc5fff68acd1bfa20b4734059c9a06cadb88119dcd5258b5b0d21d91c8/httptools-0.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f256d6ce930c52ca1cb2a960b7da03548c454e7d28b06059ad41bfe789036ce0", size = 466610, upload-time = "2026-05-25T22:17:29.816Z" },
+ { url = "https://files.pythonhosted.org/packages/60/bd/07b13c93ffd9bec9546e0d43f8e19378dd696dbd278511406bc07371ef1f/httptools-0.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:19d1ee275bb59ba2643ba9a3a1e51cc0c788caf2b8df506368e03f56fdd08527", size = 92705, upload-time = "2026-05-25T22:17:31.133Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/c4/121648f68ce066d7bd762d6b6d97e620847642d38d54f3d90ff11d947629/httptools-0.8.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:de1ed58a974e75d56560acc7e7fed01a454994429456f65209789992e41f2568", size = 215023, upload-time = "2026-05-25T22:17:32.401Z" },
+ { url = "https://files.pythonhosted.org/packages/b9/b0/312a062ae741ae3e8baa8c8bf20be81b2e67337b259ab4349bebc7b6142e/httptools-0.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e93c227b595c6926c1acee96891dd9da4be338cfbe82e5cd3bb9d8dd7dc4ac0b", size = 117405, upload-time = "2026-05-25T22:17:33.742Z" },
+ { url = "https://files.pythonhosted.org/packages/fc/37/fccd705f795386bb05bf413012fecff2a33e5aa8c2f069096de3e9fd8702/httptools-0.8.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2a021c3a8e65cc125390d72f59b968afca3bdcaff25bd67965e0a055a14946ca", size = 558497, upload-time = "2026-05-25T22:17:34.732Z" },
+ { url = "https://files.pythonhosted.org/packages/bd/39/f172e8003576de35f5ba77ff417cf0e34429d35dc014deef15afa337a72c/httptools-0.8.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:48774d39cbb70e2b1f71f88852a3087ae1d3a1eb80482bb48c13067ab080c14f", size = 571585, upload-time = "2026-05-25T22:17:35.813Z" },
+ { url = "https://files.pythonhosted.org/packages/3e/b9/f5564760af99f3dbbf3f9104dc00e5da27e96cf433c6bdcf77617f70bf3f/httptools-0.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:88eead8ec8680a9f146c655bc88445a325bd7921cfd8194c7337e9467282427d", size = 543297, upload-time = "2026-05-25T22:17:37.08Z" },
+ { url = "https://files.pythonhosted.org/packages/99/67/8d9f2c313618e161b82f3873188e7196126da1d6e29688df40eb3997c77a/httptools-0.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:2c032fa028f46871ec7e1fc59fc15e8023eab3e6bbe6ece786a1611719a5d081", size = 539535, upload-time = "2026-05-25T22:17:38.032Z" },
+ { url = "https://files.pythonhosted.org/packages/48/63/b906c01e53f50d432c0defe43ce52764a111dc1bdd028bafbeb54dcfd008/httptools-0.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:384c17174464c8e873398b7af24f0b1f44d992c820328413951a625323155d77", size = 108209, upload-time = "2026-05-25T22:17:39.473Z" },
]
[[package]]
@@ -385,14 +385,14 @@ name = "httpx"
version = "0.28.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
- { name = "anyio" },
- { name = "certifi" },
- { name = "httpcore" },
- { name = "idna" },
+ { name = "anyio" },
+ { name = "certifi" },
+ { name = "httpcore" },
+ { name = "idna" },
]
sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" },
+ { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" },
]
[[package]]
@@ -400,20 +400,20 @@ name = "huggingface-hub"
version = "1.16.4"
source = { registry = "https://pypi.org/simple" }
dependencies = [
- { name = "click" },
- { name = "filelock" },
- { name = "fsspec" },
- { name = "hf-xet", marker = "platform_machine == 'AMD64' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'arm64' or platform_machine == 'x86_64'" },
- { name = "httpx" },
- { name = "packaging" },
- { name = "pyyaml" },
- { name = "tqdm" },
- { name = "typer" },
- { name = "typing-extensions" },
+ { name = "click" },
+ { name = "filelock" },
+ { name = "fsspec" },
+ { name = "hf-xet", marker = "platform_machine == 'AMD64' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'arm64' or platform_machine == 'x86_64'" },
+ { name = "httpx" },
+ { name = "packaging" },
+ { name = "pyyaml" },
+ { name = "tqdm" },
+ { name = "typer" },
+ { name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/ab/11/9b6e439cb2417c479c3da108b38363232a1554721de9f8ef4836cb07422b/huggingface_hub-1.16.4.tar.gz", hash = "sha256:023bacd155f837d3fa56379ac8e23dababe6d6d87b04f8dacc258a44a38abe01", size = 792585, upload-time = "2026-05-26T17:19:09.971Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/da/86/e05d58ea272089151ba9f6fcc7b44a97aa2533d5a5bce46611220c23c6d6/huggingface_hub-1.16.4-py3-none-any.whl", hash = "sha256:994ec184c3330952d7b5f131ea0b1a6ba1047bd05461f5dec191f8fc1099fbd7", size = 668190, upload-time = "2026-05-26T17:19:08.228Z" },
+ { url = "https://files.pythonhosted.org/packages/da/86/e05d58ea272089151ba9f6fcc7b44a97aa2533d5a5bce46611220c23c6d6/huggingface_hub-1.16.4-py3-none-any.whl", hash = "sha256:994ec184c3330952d7b5f131ea0b1a6ba1047bd05461f5dec191f8fc1099fbd7", size = 668190, upload-time = "2026-05-26T17:19:08.228Z" },
]
[[package]]
@@ -422,7 +422,7 @@ version = "2.6.19"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/52/63/51723b5f116cc04b061cb6f5a561790abf249d25931d515cd375e063e0f4/identify-2.6.19.tar.gz", hash = "sha256:6be5020c38fcb07da56c53733538a3081ea5aa70d36a156f83044bfbf9173842", size = 99567, upload-time = "2026-04-17T18:39:50.265Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/94/84/d9273cd09688070a6523c4aee4663a8538721b2b755c4962aafae0011e72/identify-2.6.19-py2.py3-none-any.whl", hash = "sha256:20e6a87f786f768c092a721ad107fc9df0eb89347be9396cadf3f4abbd1fb78a", size = 99397, upload-time = "2026-04-17T18:39:49.221Z" },
+ { url = "https://files.pythonhosted.org/packages/94/84/d9273cd09688070a6523c4aee4663a8538721b2b755c4962aafae0011e72/identify-2.6.19-py2.py3-none-any.whl", hash = "sha256:20e6a87f786f768c092a721ad107fc9df0eb89347be9396cadf3f4abbd1fb78a", size = 99397, upload-time = "2026-04-17T18:39:49.221Z" },
]
[[package]]
@@ -431,7 +431,7 @@ version = "3.16"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/1a/88/bcf9709822fe69d02c2a6a77956c98ce6ea8ca8767a9aadcedc7eb6a2390/idna-3.16.tar.gz", hash = "sha256:d7a6da03db833450fca25d2358ac9ff06cd624577a4aea3a596d5c0f77b8e03d", size = 203770, upload-time = "2026-05-22T00:16:18.781Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/94/16/70255075a9859a0e3adb789b68ceb0e210dec03934245fd98d248226572f/idna-3.16-py3-none-any.whl", hash = "sha256:cc246e3a3f89580c3a951b5ad298ca4638078b2cdd4f115654332b5c26daded5", size = 74165, upload-time = "2026-05-22T00:16:16.698Z" },
+ { url = "https://files.pythonhosted.org/packages/94/16/70255075a9859a0e3adb789b68ceb0e210dec03934245fd98d248226572f/idna-3.16-py3-none-any.whl", hash = "sha256:cc246e3a3f89580c3a951b5ad298ca4638078b2cdd4f115654332b5c26daded5", size = 74165, upload-time = "2026-05-22T00:16:16.698Z" },
]
[[package]]
@@ -440,7 +440,7 @@ version = "2.3.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
+ { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
]
[[package]]
@@ -448,11 +448,11 @@ name = "jinja2"
version = "3.1.6"
source = { registry = "https://pypi.org/simple" }
dependencies = [
- { name = "markupsafe" },
+ { name = "markupsafe" },
]
sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" },
+ { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" },
]
[[package]]
@@ -461,110 +461,110 @@ version = "1.5.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/41/f2/d34e8b3a08a9cc79a50b2208a93dce981fe615b64d5a4d4abee421d898df/joblib-1.5.3.tar.gz", hash = "sha256:8561a3269e6801106863fd0d6d84bb737be9e7631e33aaed3fb9ce5953688da3", size = 331603, upload-time = "2025-12-15T08:41:46.427Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/7b/91/984aca2ec129e2757d1e4e3c81c3fcda9d0f85b74670a094cc443d9ee949/joblib-1.5.3-py3-none-any.whl", hash = "sha256:5fc3c5039fc5ca8c0276333a188bbd59d6b7ab37fe6632daa76bc7f9ec18e713", size = 309071, upload-time = "2025-12-15T08:41:44.973Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/91/984aca2ec129e2757d1e4e3c81c3fcda9d0f85b74670a094cc443d9ee949/joblib-1.5.3-py3-none-any.whl", hash = "sha256:5fc3c5039fc5ca8c0276333a188bbd59d6b7ab37fe6632daa76bc7f9ec18e713", size = 309071, upload-time = "2025-12-15T08:41:44.973Z" },
]
[[package]]
name = "ktx-daemon"
-version = "0.7.0"
+version = "0.8.0"
source = { editable = "python/ktx-daemon" }
dependencies = [
- { name = "fastapi" },
- { name = "ktx-sl" },
- { name = "lkml" },
- { name = "numpy" },
- { name = "orjson" },
- { name = "pandas" },
- { name = "posthog" },
- { name = "psycopg", extra = ["binary"] },
- { name = "pydantic" },
- { name = "requests" },
- { name = "sqlglot" },
- { name = "uvicorn", extra = ["standard"] },
+ { name = "fastapi" },
+ { name = "ktx-sl" },
+ { name = "lkml" },
+ { name = "numpy" },
+ { name = "orjson" },
+ { name = "pandas" },
+ { name = "posthog" },
+ { name = "psycopg", extra = ["binary"] },
+ { name = "pydantic" },
+ { name = "requests" },
+ { name = "sqlglot" },
+ { name = "uvicorn", extra = ["standard"] },
]
[package.optional-dependencies]
local-embeddings = [
- { name = "sentence-transformers" },
- { name = "torch", version = "2.12.0", source = { registry = "https://download.pytorch.org/whl/cpu" }, marker = "sys_platform == 'darwin'" },
- { name = "torch", version = "2.12.0+cpu", source = { registry = "https://download.pytorch.org/whl/cpu" }, marker = "sys_platform != 'darwin'" },
+ { name = "sentence-transformers" },
+ { name = "torch", version = "2.12.0", source = { registry = "https://download.pytorch.org/whl/cpu" }, marker = "sys_platform == 'darwin'" },
+ { name = "torch", version = "2.12.0+cpu", source = { registry = "https://download.pytorch.org/whl/cpu" }, marker = "sys_platform != 'darwin'" },
]
[package.dev-dependencies]
dev = [
- { name = "httpx" },
- { name = "pytest" },
+ { name = "httpx" },
+ { name = "pytest" },
]
[package.metadata]
requires-dist = [
- { name = "fastapi", specifier = ">=0.136.3" },
- { name = "ktx-sl", editable = "python/ktx-sl" },
- { name = "lkml", specifier = ">=1.3.7" },
- { name = "numpy", specifier = ">=2.4.6" },
- { name = "orjson", specifier = ">=3.11.9" },
- { name = "pandas", specifier = ">=3.0.3" },
- { name = "posthog", specifier = ">=7.16.1" },
- { name = "psycopg", extras = ["binary"], specifier = ">=3.3.4" },
- { name = "pydantic", specifier = ">=2.13.4" },
- { name = "requests", specifier = ">=2.34.2" },
- { name = "sentence-transformers", marker = "extra == 'local-embeddings'", specifier = ">=5.1.1" },
- { name = "sqlglot", specifier = ">=30" },
- { name = "torch", marker = "extra == 'local-embeddings'", specifier = ">=2.2.0", index = "https://download.pytorch.org/whl/cpu" },
- { name = "uvicorn", extras = ["standard"], specifier = ">=0.48.0" },
+ { name = "fastapi", specifier = ">=0.136.3" },
+ { name = "ktx-sl", editable = "python/ktx-sl" },
+ { name = "lkml", specifier = ">=1.3.7" },
+ { name = "numpy", specifier = ">=2.4.6" },
+ { name = "orjson", specifier = ">=3.11.9" },
+ { name = "pandas", specifier = ">=3.0.3" },
+ { name = "posthog", specifier = ">=7.16.1" },
+ { name = "psycopg", extras = ["binary"], specifier = ">=3.3.4" },
+ { name = "pydantic", specifier = ">=2.13.4" },
+ { name = "requests", specifier = ">=2.34.2" },
+ { name = "sentence-transformers", marker = "extra == 'local-embeddings'", specifier = ">=5.1.1" },
+ { name = "sqlglot", specifier = ">=30" },
+ { name = "torch", marker = "extra == 'local-embeddings'", specifier = ">=2.2.0", index = "https://download.pytorch.org/whl/cpu" },
+ { name = "uvicorn", extras = ["standard"], specifier = ">=0.48.0" },
]
provides-extras = ["local-embeddings"]
[package.metadata.requires-dev]
dev = [
- { name = "httpx", specifier = ">=0.28.1" },
- { name = "pytest", specifier = ">=9.0.2" },
+ { name = "httpx", specifier = ">=0.28.1" },
+ { name = "pytest", specifier = ">=9.0.2" },
]
[[package]]
name = "ktx-sl"
-version = "0.7.0"
+version = "0.8.0"
source = { editable = "python/ktx-sl" }
dependencies = [
- { name = "pydantic" },
- { name = "pyyaml" },
- { name = "sqlglot" },
+ { name = "pydantic" },
+ { name = "pyyaml" },
+ { name = "sqlglot" },
]
[package.optional-dependencies]
dev = [
- { name = "pre-commit" },
- { name = "pytest" },
- { name = "pytest-cov" },
- { name = "ruff" },
+ { name = "pre-commit" },
+ { name = "pytest" },
+ { name = "pytest-cov" },
+ { name = "ruff" },
]
tpch = [
- { name = "duckdb" },
+ { name = "duckdb" },
]
[package.dev-dependencies]
dev = [
- { name = "pytest" },
- { name = "pytest-cov" },
+ { name = "pytest" },
+ { name = "pytest-cov" },
]
[package.metadata]
requires-dist = [
- { name = "duckdb", marker = "extra == 'tpch'", specifier = ">=1.0" },
- { name = "pre-commit", marker = "extra == 'dev'" },
- { name = "pydantic", specifier = ">=2" },
- { name = "pytest", marker = "extra == 'dev'", specifier = ">=8" },
- { name = "pytest-cov", marker = "extra == 'dev'" },
- { name = "pyyaml", specifier = ">=6" },
- { name = "ruff", marker = "extra == 'dev'" },
- { name = "sqlglot", specifier = ">=30" },
+ { name = "duckdb", marker = "extra == 'tpch'", specifier = ">=1.0" },
+ { name = "pre-commit", marker = "extra == 'dev'" },
+ { name = "pydantic", specifier = ">=2" },
+ { name = "pytest", marker = "extra == 'dev'", specifier = ">=8" },
+ { name = "pytest-cov", marker = "extra == 'dev'" },
+ { name = "pyyaml", specifier = ">=6" },
+ { name = "ruff", marker = "extra == 'dev'" },
+ { name = "sqlglot", specifier = ">=30" },
]
provides-extras = ["dev", "tpch"]
[package.metadata.requires-dev]
dev = [
- { name = "pytest", specifier = ">=9.0.2" },
- { name = "pytest-cov", specifier = ">=7.1.0" },
+ { name = "pytest", specifier = ">=9.0.2" },
+ { name = "pytest-cov", specifier = ">=7.1.0" },
]
[[package]]
@@ -574,19 +574,20 @@ source = { virtual = "." }
[package.dev-dependencies]
dev = [
- { name = "pre-commit" },
- { name = "pytest" },
- { name = "pytest-cov" },
- { name = "ruff" },
+ { name = "pre-commit" },
+ { name = "pytest" },
+ { name = "pytest-cov" },
+ { name = "ruff" },
]
[package.metadata]
+
[package.metadata.requires-dev]
dev = [
- { name = "pre-commit", specifier = ">=4.6.0" },
- { name = "pytest", specifier = ">=9.0.2" },
- { name = "pytest-cov", specifier = ">=7.1.0" },
- { name = "ruff", specifier = ">=0.8.4" },
+ { name = "pre-commit", specifier = ">=4.6.0" },
+ { name = "pytest", specifier = ">=9.0.2" },
+ { name = "pytest-cov", specifier = ">=7.1.0" },
+ { name = "ruff", specifier = ">=0.8.4" },
]
[[package]]
@@ -595,7 +596,7 @@ version = "1.3.7"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/bf/18/18a3d0281c5e209156b877796096d4ac7259f03465409673056386c99221/lkml-1.3.7.tar.gz", hash = "sha256:51dc9f1b7e74cd7a00e0dbbf06fb573952015328f1f4a3a0730d444444a8ae7a", size = 28763, upload-time = "2025-01-31T02:30:35.472Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/c7/15/e7124d4ec54fdcafa801b55d6b67d6196ed6c8a0de554e1a8b67b66fec65/lkml-1.3.7-py2.py3-none-any.whl", hash = "sha256:ce54c517f81fbd21d452038be9e2504fa02951a5bc30f7d7f1eb552c1f3f2b39", size = 23062, upload-time = "2025-01-31T02:30:34.377Z" },
+ { url = "https://files.pythonhosted.org/packages/c7/15/e7124d4ec54fdcafa801b55d6b67d6196ed6c8a0de554e1a8b67b66fec65/lkml-1.3.7-py2.py3-none-any.whl", hash = "sha256:ce54c517f81fbd21d452038be9e2504fa02951a5bc30f7d7f1eb552c1f3f2b39", size = 23062, upload-time = "2025-01-31T02:30:34.377Z" },
]
[[package]]
@@ -603,11 +604,11 @@ name = "markdown-it-py"
version = "4.2.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
- { name = "mdurl" },
+ { name = "mdurl" },
]
sdist = { url = "https://files.pythonhosted.org/packages/06/ff/7841249c247aa650a76b9ee4bbaeae59370dc8bfd2f6c01f3630c35eb134/markdown_it_py-4.2.0.tar.gz", hash = "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49", size = 82454, upload-time = "2026-05-07T12:08:28.36Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a", size = 91687, upload-time = "2026-05-07T12:08:27.182Z" },
+ { url = "https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a", size = 91687, upload-time = "2026-05-07T12:08:27.182Z" },
]
[[package]]
@@ -616,50 +617,50 @@ version = "3.0.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" },
- { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" },
- { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" },
- { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" },
- { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" },
- { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" },
- { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" },
- { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" },
- { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" },
- { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" },
- { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" },
- { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" },
- { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" },
- { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" },
- { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" },
- { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" },
- { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" },
- { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" },
- { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" },
- { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" },
- { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" },
- { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" },
- { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" },
- { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" },
- { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" },
- { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" },
- { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" },
- { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" },
- { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" },
- { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" },
- { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" },
- { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" },
- { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" },
- { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" },
- { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" },
- { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" },
- { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" },
- { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" },
- { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" },
- { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" },
- { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" },
- { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" },
- { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" },
- { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" },
+ { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" },
+ { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" },
+ { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" },
+ { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" },
+ { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" },
+ { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" },
+ { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" },
+ { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" },
+ { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" },
+ { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" },
+ { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" },
+ { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" },
+ { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" },
+ { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" },
+ { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" },
+ { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" },
+ { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" },
+ { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" },
+ { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" },
+ { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" },
+ { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" },
+ { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" },
+ { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" },
+ { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" },
+ { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" },
+ { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" },
+ { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" },
+ { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" },
+ { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" },
+ { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" },
+ { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" },
+ { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" },
+ { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" },
+ { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" },
+ { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" },
+ { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" },
+ { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" },
+ { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" },
+ { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" },
]
[[package]]
@@ -668,7 +669,7 @@ version = "0.1.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" },
+ { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" },
]
[[package]]
@@ -677,7 +678,7 @@ version = "1.3.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/e0/47/dd32fa426cc72114383ac549964eecb20ecfd886d1e5ccf5340b55b02f57/mpmath-1.3.0.tar.gz", hash = "sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f", size = 508106, upload-time = "2023-03-07T16:47:11.061Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c", size = 536198, upload-time = "2023-03-07T16:47:09.197Z" },
+ { url = "https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c", size = 536198, upload-time = "2023-03-07T16:47:09.197Z" },
]
[[package]]
@@ -686,7 +687,7 @@ version = "3.6.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/6a/51/63fe664f3908c97be9d2e4f1158eb633317598cfa6e1fc14af5383f17512/networkx-3.6.1.tar.gz", hash = "sha256:26b7c357accc0c8cde558ad486283728b65b6a95d85ee1cd66bafab4c8168509", size = 2517025, upload-time = "2025-12-08T17:02:39.908Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/9e/c9/b2622292ea83fbb4ec318f5b9ab867d0a28ab43c5717bb85b0a5f6b3b0a4/networkx-3.6.1-py3-none-any.whl", hash = "sha256:d47fbf302e7d9cbbb9e2555a0d267983d2aa476bac30e90dfbe5669bd57f3762", size = 2068504, upload-time = "2025-12-08T17:02:38.159Z" },
+ { url = "https://files.pythonhosted.org/packages/9e/c9/b2622292ea83fbb4ec318f5b9ab867d0a28ab43c5717bb85b0a5f6b3b0a4/networkx-3.6.1-py3-none-any.whl", hash = "sha256:d47fbf302e7d9cbbb9e2555a0d267983d2aa476bac30e90dfbe5669bd57f3762", size = 2068504, upload-time = "2025-12-08T17:02:38.159Z" },
]
[[package]]
@@ -695,7 +696,7 @@ version = "1.10.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/24/bf/d1bda4f6168e0b2e9e5958945e01910052158313224ada5ce1fb2e1113b8/nodeenv-1.10.0.tar.gz", hash = "sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb", size = 55611, upload-time = "2025-12-20T14:08:54.006Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827", size = 23438, upload-time = "2025-12-20T14:08:52.782Z" },
+ { url = "https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827", size = 23438, upload-time = "2025-12-20T14:08:52.782Z" },
]
[[package]]
@@ -704,48 +705,48 @@ version = "2.4.6"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d0/ad/fed0499ce6a338d2a03ebae59cd15093910c8875328855781952abf6c2fe/numpy-2.4.6.tar.gz", hash = "sha256:f3a3570c4a2a16746ac2c31a7c7c7b0c186b95ce902e33db6f28094ed7387dda", size = 20735807, upload-time = "2026-05-18T23:37:14.07Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/fb/82/bdab26d7438c6791ca31b7c024ca37c1eab8b726ba236129005cd4a06e45/numpy-2.4.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:511dbaf848decaaaf4b4ca48032619fb3138710c4bf7da7617765edad1ef96b0", size = 16684648, upload-time = "2026-05-18T23:34:29.41Z" },
- { url = "https://files.pythonhosted.org/packages/1b/30/a80189bcc7f5e4258b3fbc3968d909d1756f54d023299ecc39ad6fdb9ef8/numpy-2.4.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bf162abab1c1a736333192707cef898e735a5ca00f38f27eeedf44b39d9e85eb", size = 14693902, upload-time = "2026-05-18T23:34:33.013Z" },
- { url = "https://files.pythonhosted.org/packages/97/12/70b5d0d7c15e1ebb8a6a84a8caa1d19e181d84fb58bb6d70aca29099dec1/numpy-2.4.6-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:043191bfa8eab18c776647b62723ac9dddece59743b13f49b2016094129c2b3f", size = 5198992, upload-time = "2026-05-18T23:34:36.132Z" },
- { url = "https://files.pythonhosted.org/packages/ba/8c/ebd2a8f8a83541f8d38cc5667e8c2b69cecfd30da6e45693e8158857d44b/numpy-2.4.6-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:6180d8b35af935aed8ece3a85e0a43f87393ae0ac87c8d2c8bd2c993f7270ef3", size = 6546944, upload-time = "2026-05-18T23:34:38.484Z" },
- { url = "https://files.pythonhosted.org/packages/bb/c5/7b863a97a91671a0338f4253bd3b5a3d3852f0692dae91711c9f4a10e787/numpy-2.4.6-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:72fbe16c6fac95aedf5937fa873445cec2110be35d8a4e9433d7501fd98dae6b", size = 15669392, upload-time = "2026-05-18T23:34:41.257Z" },
- { url = "https://files.pythonhosted.org/packages/a5/9d/3584b9984ca4c047aea75214ce1a4c4c73d849bd71b604264b7f5653f8a8/numpy-2.4.6-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a7830bab239b79cda9c08c2da014761cafb48da6150e1da17ac06283f43b6089", size = 16633220, upload-time = "2026-05-18T23:34:45.075Z" },
- { url = "https://files.pythonhosted.org/packages/05/ae/7c67fba23bd98caec7c99261f3a16072ade14813486b0282cb29846de832/numpy-2.4.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ef4aea96ce4d3b074422cb4f2f64e216bf9e213004bb58ecfdf50ea02ea8eb9a", size = 17020800, upload-time = "2026-05-18T23:34:49.065Z" },
- { url = "https://files.pythonhosted.org/packages/d9/5d/3b6725cb31d983c5e66916f5d36f6d7e5521129e4c4404d64f918292a5b6/numpy-2.4.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:dfa20cc6ca228e6b155b11da03825975ce66aea520985dbbddf0f2a5a495c605", size = 18357600, upload-time = "2026-05-18T23:34:52.709Z" },
- { url = "https://files.pythonhosted.org/packages/f7/da/2ccc6c2fe8898dee01d90c75c5f5f914a23daf99e3e0f59516a08760c8b5/numpy-2.4.6-cp313-cp313-win32.whl", hash = "sha256:56b39e5e0622a09a25bf5baf62f4bcf0cb8a41ae6e2819cf49bbc5a74c083f91", size = 5961134, upload-time = "2026-05-18T23:34:55.618Z" },
- { url = "https://files.pythonhosted.org/packages/b5/cd/9cc4dc876fb065d5c220aae4d5e14826b2715331bb7618ce1fb07a679d99/numpy-2.4.6-cp313-cp313-win_amd64.whl", hash = "sha256:c4fc99836233ea196540b17ab0983aff60ed07941751930f5f4d05bc3b3b7359", size = 12318598, upload-time = "2026-05-18T23:34:58.928Z" },
- { url = "https://files.pythonhosted.org/packages/39/1e/c0bcba1f8694116485fe28fd1be698c278fcda4141c5b0e53a2aed8b12a8/numpy-2.4.6-cp313-cp313-win_arm64.whl", hash = "sha256:a7c711e21628b52034bb5ab8d1bce291f752fcc5e92accc615778acee1ff4778", size = 10222272, upload-time = "2026-05-18T23:35:02.167Z" },
- { url = "https://files.pythonhosted.org/packages/63/6d/cc5619247c8f4204e507f5883528372e4ac4bb189e579fb859a12e480b1f/numpy-2.4.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:112b06a867b235ef466ed3508ddf0238050df9c727cafb5301ac385b899189a1", size = 14821197, upload-time = "2026-05-18T23:35:05.468Z" },
- { url = "https://files.pythonhosted.org/packages/00/58/f1c39161c87d9e9bed660f1ed4bafc0e403d5ec9650b6dd77aead07d489b/numpy-2.4.6-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:eaf7fa2de5c0be8ae6ff8e9bea2ccd725e980541244521d8d4b5f3354a27babe", size = 5326287, upload-time = "2026-05-18T23:35:08.693Z" },
- { url = "https://files.pythonhosted.org/packages/af/57/3917ab0fd97f271a8694513581b8a36c655f111c446852c302f04ccdb6fc/numpy-2.4.6-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:7265a2f3d436e54ef9f2b52b5c937e6be778781bd97a590319d7348f1c1ca997", size = 6646763, upload-time = "2026-05-18T23:35:11.459Z" },
- { url = "https://files.pythonhosted.org/packages/eb/0f/037e64c494b67581ae18193d770adef354c41f3f2c8ebf865602d949bf8f/numpy-2.4.6-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f74a575920ab21fe304421a3fc28793d82e299cae9eccb37084e9fc7f3617c20", size = 15728070, upload-time = "2026-05-18T23:35:14.79Z" },
- { url = "https://files.pythonhosted.org/packages/21/a6/5d2bae9c9542eb4df16dc9c46dc79c186e9bad53805dfa5399a6023c6db0/numpy-2.4.6-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ede83e07a75dd06bc501566c1eca2afc0d61677c1472ac9ad93fdee6e638a48d", size = 16681752, upload-time = "2026-05-18T23:35:18.836Z" },
- { url = "https://files.pythonhosted.org/packages/92/14/23d1dfb410ae362cd59ce53e936b1513d545eb40db3949ced632e19a459e/numpy-2.4.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:68bb27509ac1b9a3443094260f6326150663b06abe40b73a2f81160623da5b67", size = 17086024, upload-time = "2026-05-18T23:35:22.52Z" },
- { url = "https://files.pythonhosted.org/packages/4b/6e/23595a2c642cdf3bc567877064bdd7f91c8b0038a4453cf2daf7248eafe9/numpy-2.4.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:a0df0043bdb289bde1f62da130d20df23d58b45429f752bc7a8fc5325a225ecd", size = 18403398, upload-time = "2026-05-18T23:35:26.398Z" },
- { url = "https://files.pythonhosted.org/packages/8a/90/0ac3bc947217e66dec77e7cbc6a1979d1af70b6461b82f620d3bccd5e4c8/numpy-2.4.6-cp313-cp313t-win32.whl", hash = "sha256:29a287e0cf63ff528da061de6b9f64a4618da591ca1046aafc54062e40ca7eab", size = 6084971, upload-time = "2026-05-18T23:35:29.387Z" },
- { url = "https://files.pythonhosted.org/packages/77/71/5673e351671a1d2bd6063b91b44f70c0affea7d1516fa7a6572941ba4aa1/numpy-2.4.6-cp313-cp313t-win_amd64.whl", hash = "sha256:25c692919ac5a01f170a3bfcd62d745b24fd095c353d50812637d6fcab442e75", size = 12458532, upload-time = "2026-05-18T23:35:32.175Z" },
- { url = "https://files.pythonhosted.org/packages/3f/88/19d3503c5046e688f049274b27a3ef3d771152fa80d3ba3d01a3dff61abe/numpy-2.4.6-cp313-cp313t-win_arm64.whl", hash = "sha256:1e978ec1e8bd0e0e4de6bb75de9d30cbb74db6b6a2bb727618613703ca0167dd", size = 10291881, upload-time = "2026-05-18T23:35:35.465Z" },
- { url = "https://files.pythonhosted.org/packages/f8/91/3ab2044d05fd16d343c5ac2e69b127f1b2854040dd20b193257c78028bd3/numpy-2.4.6-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:06ca2f61ec4385a07a6977c55ba998a4466c123642b4a32694d3128fce18c079", size = 16683458, upload-time = "2026-05-18T23:35:38.353Z" },
- { url = "https://files.pythonhosted.org/packages/8e/62/764ce66fa4147ae6d73071a3abf804ffe606f174618697c571acdf26a7c9/numpy-2.4.6-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:38efbc8de75c7a0fc1ac190162d892787f3f47b57cc291231aafee36b80982b7", size = 14704559, upload-time = "2026-05-18T23:35:42.14Z" },
- { url = "https://files.pythonhosted.org/packages/60/61/23f27c172f022e04025b7dc2367f4d63c1a398120607ec896228649a6f48/numpy-2.4.6-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:d581b735e177fdcdce6fed8e7e8880a3fb6ee4e3653a3ac6af01c6f4c03effc5", size = 5209716, upload-time = "2026-05-18T23:35:45.377Z" },
- { url = "https://files.pythonhosted.org/packages/03/71/21cf70dc6ea3e3acb95fc53a265b2fc248b981f0194ceb5b475271b8809d/numpy-2.4.6-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:0a041d3d761dc3c35cc56ce0351506a02bcbc25f7b169f652435141a17db9096", size = 6543947, upload-time = "2026-05-18T23:35:47.926Z" },
- { url = "https://files.pythonhosted.org/packages/d5/91/64288395ee1799bd2e0b04a305dce9666da90c961e1f3fe982a05ee1c036/numpy-2.4.6-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:40fdc1ae7125e518ea98e53e69a4ebc27e1fd50510c47b7ea130cf21e5e1d42b", size = 15685197, upload-time = "2026-05-18T23:35:50.863Z" },
- { url = "https://files.pythonhosted.org/packages/f3/eb/ebffaa97dc55502df69584a8f0dcf07f69a3e0b3e2323670a2722db9aa39/numpy-2.4.6-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a2c306dea656c12c68f51f4cea133cbe78ca7435eb28c735eac1d3ebe73be6e8", size = 16638245, upload-time = "2026-05-18T23:35:54.752Z" },
- { url = "https://files.pythonhosted.org/packages/b8/0b/54f9da33128d7e350fab89c7455902eeae70349ee52bddb448dc4a576f45/numpy-2.4.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:33111801a01c12a8a1e3721f0a9232f8cfc8ae2c6b7098167e6f623c6073f402", size = 17036587, upload-time = "2026-05-18T23:35:58.355Z" },
- { url = "https://files.pythonhosted.org/packages/b6/f0/fdebc1052db1cc37c64beb22072d67cd6d1c71adca1299f53dec2b5e20d3/numpy-2.4.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ae506e6902902557576a26ff33eda8695e7ecb3cb36c3b573a0765dee114ebdb", size = 18363226, upload-time = "2026-05-18T23:36:02.845Z" },
- { url = "https://files.pythonhosted.org/packages/aa/b4/298628d98c72b57e57f7165ae6a481a1deaf6f3c28262a6e4c739c275930/numpy-2.4.6-cp314-cp314-win32.whl", hash = "sha256:aaf159caa35993cb1f56fb9b8e4610d35758e7ca005412eb1daa856a78c9c4b1", size = 6010196, upload-time = "2026-05-18T23:36:05.92Z" },
- { url = "https://files.pythonhosted.org/packages/df/ac/46de6dda46478f7942f839e094970be2d4a861e005c4b3bf07c92e291a09/numpy-2.4.6-cp314-cp314-win_amd64.whl", hash = "sha256:b507f5c4c1d508876d1819b6bf9a49d365b96320b5d4993426b33a23ca4b8261", size = 12450334, upload-time = "2026-05-18T23:36:09.107Z" },
- { url = "https://files.pythonhosted.org/packages/78/92/b8b798ac784102c0da830d2257d59358e3d3d90d1e2b3f2575dad976c5cf/numpy-2.4.6-cp314-cp314-win_arm64.whl", hash = "sha256:6f41ae150c4e32db4f3310cdaf64b1593a03dbabe29eec77fc9b50fe64061df6", size = 10495678, upload-time = "2026-05-18T23:36:12.766Z" },
- { url = "https://files.pythonhosted.org/packages/30/34/ec28d1aa8115971537c01469ab2011ee96827930f0a124de1000cc2a7ed7/numpy-2.4.6-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ece3d2cfe132e7d51f44a832b303895e6f2d499c5e74dfbdb06ee246147a304a", size = 14823672, upload-time = "2026-05-18T23:36:16.473Z" },
- { url = "https://files.pythonhosted.org/packages/16/bd/f6d1fede4e54e8042a7ff97bb495510f3c220f94bcd9e8b228e87c92cc0d/numpy-2.4.6-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:e3e5193ef5a3dc73bceee50f7fdc2c90dbb76c42df8d8fae3d1067a583df579e", size = 5328731, upload-time = "2026-05-18T23:36:19.767Z" },
- { url = "https://files.pythonhosted.org/packages/f4/f0/e105b9e2fd728a9910103884decd6951d9dd73896b914a98d9a231de02ee/numpy-2.4.6-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:17f9ade344e7d9b464a084d69bcf18fc691cb1db67c62ed80820bf4926d78f0e", size = 6649805, upload-time = "2026-05-18T23:36:22.266Z" },
- { url = "https://files.pythonhosted.org/packages/82/dd/1206a7ca6ab15e3f02069707ca96222e202af681bb73756da7527f3cb837/numpy-2.4.6-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9cd5ffd25db4e7ba6a375693b3fc0fc1791ec636c17db3720da19bde7180ec43", size = 15730496, upload-time = "2026-05-18T23:36:25.713Z" },
- { url = "https://files.pythonhosted.org/packages/51/e7/38d3ea825dcab85a591734decb2f6c67caa7c8367d374df1a1c3842f9b07/numpy-2.4.6-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7d92c3819208a60205a12a245c91ad70cb0a85336659b19b834205573ac8456e", size = 16679616, upload-time = "2026-05-18T23:36:29.652Z" },
- { url = "https://files.pythonhosted.org/packages/93/b7/caabfdf53edf663e0b4eb74d7d405d83baef09eb5e83bcd32d601d72b93e/numpy-2.4.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e85b752a1e912b70eaad4fafbd4d1238007ab221de2009b9a2f5ae7461239895", size = 17085145, upload-time = "2026-05-18T23:36:33.449Z" },
- { url = "https://files.pythonhosted.org/packages/f9/45/68d7c33a6bcf3e5aa3bdbd57a367e6f615286dfd6482f97e8ffeb734306e/numpy-2.4.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:29cb7f67d10b479ff07c17d33e39f78c07f71c40ef30d63c153d340e96cd3fb4", size = 18403813, upload-time = "2026-05-18T23:36:37.369Z" },
- { url = "https://files.pythonhosted.org/packages/9c/50/0753655aa844c99cd9e018aacf76f130f1bd81d881bb74bc0aef5d73a8ba/numpy-2.4.6-cp314-cp314t-win32.whl", hash = "sha256:260a5d70215b61ab4fadf5c7baacd64821842975eea312125ed3c39a6391b063", size = 6156982, upload-time = "2026-05-18T23:36:40.817Z" },
- { url = "https://files.pythonhosted.org/packages/b2/d4/7c67becf668f973cb490cec3e98dfd799d866f9c989a54d355672cfa0db6/numpy-2.4.6-cp314-cp314t-win_amd64.whl", hash = "sha256:81a1cca95ed5bb92aa8b10dd2cdc9a0d3853a50fad926c28b5d7e8ea54389627", size = 12638908, upload-time = "2026-05-18T23:36:43.996Z" },
- { url = "https://files.pythonhosted.org/packages/43/bb/e1c71a4295b1b1d1393d50dbb4f2a36283c6859d9d3892e84f00ec5a91d5/numpy-2.4.6-cp314-cp314t-win_arm64.whl", hash = "sha256:0c9136e14ed34a9e343a31c533d78a9813a69a3148332bce5e9821cb2f996e66", size = 10565867, upload-time = "2026-05-18T23:36:47.114Z" },
+ { url = "https://files.pythonhosted.org/packages/fb/82/bdab26d7438c6791ca31b7c024ca37c1eab8b726ba236129005cd4a06e45/numpy-2.4.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:511dbaf848decaaaf4b4ca48032619fb3138710c4bf7da7617765edad1ef96b0", size = 16684648, upload-time = "2026-05-18T23:34:29.41Z" },
+ { url = "https://files.pythonhosted.org/packages/1b/30/a80189bcc7f5e4258b3fbc3968d909d1756f54d023299ecc39ad6fdb9ef8/numpy-2.4.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bf162abab1c1a736333192707cef898e735a5ca00f38f27eeedf44b39d9e85eb", size = 14693902, upload-time = "2026-05-18T23:34:33.013Z" },
+ { url = "https://files.pythonhosted.org/packages/97/12/70b5d0d7c15e1ebb8a6a84a8caa1d19e181d84fb58bb6d70aca29099dec1/numpy-2.4.6-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:043191bfa8eab18c776647b62723ac9dddece59743b13f49b2016094129c2b3f", size = 5198992, upload-time = "2026-05-18T23:34:36.132Z" },
+ { url = "https://files.pythonhosted.org/packages/ba/8c/ebd2a8f8a83541f8d38cc5667e8c2b69cecfd30da6e45693e8158857d44b/numpy-2.4.6-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:6180d8b35af935aed8ece3a85e0a43f87393ae0ac87c8d2c8bd2c993f7270ef3", size = 6546944, upload-time = "2026-05-18T23:34:38.484Z" },
+ { url = "https://files.pythonhosted.org/packages/bb/c5/7b863a97a91671a0338f4253bd3b5a3d3852f0692dae91711c9f4a10e787/numpy-2.4.6-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:72fbe16c6fac95aedf5937fa873445cec2110be35d8a4e9433d7501fd98dae6b", size = 15669392, upload-time = "2026-05-18T23:34:41.257Z" },
+ { url = "https://files.pythonhosted.org/packages/a5/9d/3584b9984ca4c047aea75214ce1a4c4c73d849bd71b604264b7f5653f8a8/numpy-2.4.6-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a7830bab239b79cda9c08c2da014761cafb48da6150e1da17ac06283f43b6089", size = 16633220, upload-time = "2026-05-18T23:34:45.075Z" },
+ { url = "https://files.pythonhosted.org/packages/05/ae/7c67fba23bd98caec7c99261f3a16072ade14813486b0282cb29846de832/numpy-2.4.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ef4aea96ce4d3b074422cb4f2f64e216bf9e213004bb58ecfdf50ea02ea8eb9a", size = 17020800, upload-time = "2026-05-18T23:34:49.065Z" },
+ { url = "https://files.pythonhosted.org/packages/d9/5d/3b6725cb31d983c5e66916f5d36f6d7e5521129e4c4404d64f918292a5b6/numpy-2.4.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:dfa20cc6ca228e6b155b11da03825975ce66aea520985dbbddf0f2a5a495c605", size = 18357600, upload-time = "2026-05-18T23:34:52.709Z" },
+ { url = "https://files.pythonhosted.org/packages/f7/da/2ccc6c2fe8898dee01d90c75c5f5f914a23daf99e3e0f59516a08760c8b5/numpy-2.4.6-cp313-cp313-win32.whl", hash = "sha256:56b39e5e0622a09a25bf5baf62f4bcf0cb8a41ae6e2819cf49bbc5a74c083f91", size = 5961134, upload-time = "2026-05-18T23:34:55.618Z" },
+ { url = "https://files.pythonhosted.org/packages/b5/cd/9cc4dc876fb065d5c220aae4d5e14826b2715331bb7618ce1fb07a679d99/numpy-2.4.6-cp313-cp313-win_amd64.whl", hash = "sha256:c4fc99836233ea196540b17ab0983aff60ed07941751930f5f4d05bc3b3b7359", size = 12318598, upload-time = "2026-05-18T23:34:58.928Z" },
+ { url = "https://files.pythonhosted.org/packages/39/1e/c0bcba1f8694116485fe28fd1be698c278fcda4141c5b0e53a2aed8b12a8/numpy-2.4.6-cp313-cp313-win_arm64.whl", hash = "sha256:a7c711e21628b52034bb5ab8d1bce291f752fcc5e92accc615778acee1ff4778", size = 10222272, upload-time = "2026-05-18T23:35:02.167Z" },
+ { url = "https://files.pythonhosted.org/packages/63/6d/cc5619247c8f4204e507f5883528372e4ac4bb189e579fb859a12e480b1f/numpy-2.4.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:112b06a867b235ef466ed3508ddf0238050df9c727cafb5301ac385b899189a1", size = 14821197, upload-time = "2026-05-18T23:35:05.468Z" },
+ { url = "https://files.pythonhosted.org/packages/00/58/f1c39161c87d9e9bed660f1ed4bafc0e403d5ec9650b6dd77aead07d489b/numpy-2.4.6-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:eaf7fa2de5c0be8ae6ff8e9bea2ccd725e980541244521d8d4b5f3354a27babe", size = 5326287, upload-time = "2026-05-18T23:35:08.693Z" },
+ { url = "https://files.pythonhosted.org/packages/af/57/3917ab0fd97f271a8694513581b8a36c655f111c446852c302f04ccdb6fc/numpy-2.4.6-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:7265a2f3d436e54ef9f2b52b5c937e6be778781bd97a590319d7348f1c1ca997", size = 6646763, upload-time = "2026-05-18T23:35:11.459Z" },
+ { url = "https://files.pythonhosted.org/packages/eb/0f/037e64c494b67581ae18193d770adef354c41f3f2c8ebf865602d949bf8f/numpy-2.4.6-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f74a575920ab21fe304421a3fc28793d82e299cae9eccb37084e9fc7f3617c20", size = 15728070, upload-time = "2026-05-18T23:35:14.79Z" },
+ { url = "https://files.pythonhosted.org/packages/21/a6/5d2bae9c9542eb4df16dc9c46dc79c186e9bad53805dfa5399a6023c6db0/numpy-2.4.6-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ede83e07a75dd06bc501566c1eca2afc0d61677c1472ac9ad93fdee6e638a48d", size = 16681752, upload-time = "2026-05-18T23:35:18.836Z" },
+ { url = "https://files.pythonhosted.org/packages/92/14/23d1dfb410ae362cd59ce53e936b1513d545eb40db3949ced632e19a459e/numpy-2.4.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:68bb27509ac1b9a3443094260f6326150663b06abe40b73a2f81160623da5b67", size = 17086024, upload-time = "2026-05-18T23:35:22.52Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/6e/23595a2c642cdf3bc567877064bdd7f91c8b0038a4453cf2daf7248eafe9/numpy-2.4.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:a0df0043bdb289bde1f62da130d20df23d58b45429f752bc7a8fc5325a225ecd", size = 18403398, upload-time = "2026-05-18T23:35:26.398Z" },
+ { url = "https://files.pythonhosted.org/packages/8a/90/0ac3bc947217e66dec77e7cbc6a1979d1af70b6461b82f620d3bccd5e4c8/numpy-2.4.6-cp313-cp313t-win32.whl", hash = "sha256:29a287e0cf63ff528da061de6b9f64a4618da591ca1046aafc54062e40ca7eab", size = 6084971, upload-time = "2026-05-18T23:35:29.387Z" },
+ { url = "https://files.pythonhosted.org/packages/77/71/5673e351671a1d2bd6063b91b44f70c0affea7d1516fa7a6572941ba4aa1/numpy-2.4.6-cp313-cp313t-win_amd64.whl", hash = "sha256:25c692919ac5a01f170a3bfcd62d745b24fd095c353d50812637d6fcab442e75", size = 12458532, upload-time = "2026-05-18T23:35:32.175Z" },
+ { url = "https://files.pythonhosted.org/packages/3f/88/19d3503c5046e688f049274b27a3ef3d771152fa80d3ba3d01a3dff61abe/numpy-2.4.6-cp313-cp313t-win_arm64.whl", hash = "sha256:1e978ec1e8bd0e0e4de6bb75de9d30cbb74db6b6a2bb727618613703ca0167dd", size = 10291881, upload-time = "2026-05-18T23:35:35.465Z" },
+ { url = "https://files.pythonhosted.org/packages/f8/91/3ab2044d05fd16d343c5ac2e69b127f1b2854040dd20b193257c78028bd3/numpy-2.4.6-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:06ca2f61ec4385a07a6977c55ba998a4466c123642b4a32694d3128fce18c079", size = 16683458, upload-time = "2026-05-18T23:35:38.353Z" },
+ { url = "https://files.pythonhosted.org/packages/8e/62/764ce66fa4147ae6d73071a3abf804ffe606f174618697c571acdf26a7c9/numpy-2.4.6-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:38efbc8de75c7a0fc1ac190162d892787f3f47b57cc291231aafee36b80982b7", size = 14704559, upload-time = "2026-05-18T23:35:42.14Z" },
+ { url = "https://files.pythonhosted.org/packages/60/61/23f27c172f022e04025b7dc2367f4d63c1a398120607ec896228649a6f48/numpy-2.4.6-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:d581b735e177fdcdce6fed8e7e8880a3fb6ee4e3653a3ac6af01c6f4c03effc5", size = 5209716, upload-time = "2026-05-18T23:35:45.377Z" },
+ { url = "https://files.pythonhosted.org/packages/03/71/21cf70dc6ea3e3acb95fc53a265b2fc248b981f0194ceb5b475271b8809d/numpy-2.4.6-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:0a041d3d761dc3c35cc56ce0351506a02bcbc25f7b169f652435141a17db9096", size = 6543947, upload-time = "2026-05-18T23:35:47.926Z" },
+ { url = "https://files.pythonhosted.org/packages/d5/91/64288395ee1799bd2e0b04a305dce9666da90c961e1f3fe982a05ee1c036/numpy-2.4.6-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:40fdc1ae7125e518ea98e53e69a4ebc27e1fd50510c47b7ea130cf21e5e1d42b", size = 15685197, upload-time = "2026-05-18T23:35:50.863Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/eb/ebffaa97dc55502df69584a8f0dcf07f69a3e0b3e2323670a2722db9aa39/numpy-2.4.6-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a2c306dea656c12c68f51f4cea133cbe78ca7435eb28c735eac1d3ebe73be6e8", size = 16638245, upload-time = "2026-05-18T23:35:54.752Z" },
+ { url = "https://files.pythonhosted.org/packages/b8/0b/54f9da33128d7e350fab89c7455902eeae70349ee52bddb448dc4a576f45/numpy-2.4.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:33111801a01c12a8a1e3721f0a9232f8cfc8ae2c6b7098167e6f623c6073f402", size = 17036587, upload-time = "2026-05-18T23:35:58.355Z" },
+ { url = "https://files.pythonhosted.org/packages/b6/f0/fdebc1052db1cc37c64beb22072d67cd6d1c71adca1299f53dec2b5e20d3/numpy-2.4.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ae506e6902902557576a26ff33eda8695e7ecb3cb36c3b573a0765dee114ebdb", size = 18363226, upload-time = "2026-05-18T23:36:02.845Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/b4/298628d98c72b57e57f7165ae6a481a1deaf6f3c28262a6e4c739c275930/numpy-2.4.6-cp314-cp314-win32.whl", hash = "sha256:aaf159caa35993cb1f56fb9b8e4610d35758e7ca005412eb1daa856a78c9c4b1", size = 6010196, upload-time = "2026-05-18T23:36:05.92Z" },
+ { url = "https://files.pythonhosted.org/packages/df/ac/46de6dda46478f7942f839e094970be2d4a861e005c4b3bf07c92e291a09/numpy-2.4.6-cp314-cp314-win_amd64.whl", hash = "sha256:b507f5c4c1d508876d1819b6bf9a49d365b96320b5d4993426b33a23ca4b8261", size = 12450334, upload-time = "2026-05-18T23:36:09.107Z" },
+ { url = "https://files.pythonhosted.org/packages/78/92/b8b798ac784102c0da830d2257d59358e3d3d90d1e2b3f2575dad976c5cf/numpy-2.4.6-cp314-cp314-win_arm64.whl", hash = "sha256:6f41ae150c4e32db4f3310cdaf64b1593a03dbabe29eec77fc9b50fe64061df6", size = 10495678, upload-time = "2026-05-18T23:36:12.766Z" },
+ { url = "https://files.pythonhosted.org/packages/30/34/ec28d1aa8115971537c01469ab2011ee96827930f0a124de1000cc2a7ed7/numpy-2.4.6-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ece3d2cfe132e7d51f44a832b303895e6f2d499c5e74dfbdb06ee246147a304a", size = 14823672, upload-time = "2026-05-18T23:36:16.473Z" },
+ { url = "https://files.pythonhosted.org/packages/16/bd/f6d1fede4e54e8042a7ff97bb495510f3c220f94bcd9e8b228e87c92cc0d/numpy-2.4.6-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:e3e5193ef5a3dc73bceee50f7fdc2c90dbb76c42df8d8fae3d1067a583df579e", size = 5328731, upload-time = "2026-05-18T23:36:19.767Z" },
+ { url = "https://files.pythonhosted.org/packages/f4/f0/e105b9e2fd728a9910103884decd6951d9dd73896b914a98d9a231de02ee/numpy-2.4.6-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:17f9ade344e7d9b464a084d69bcf18fc691cb1db67c62ed80820bf4926d78f0e", size = 6649805, upload-time = "2026-05-18T23:36:22.266Z" },
+ { url = "https://files.pythonhosted.org/packages/82/dd/1206a7ca6ab15e3f02069707ca96222e202af681bb73756da7527f3cb837/numpy-2.4.6-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9cd5ffd25db4e7ba6a375693b3fc0fc1791ec636c17db3720da19bde7180ec43", size = 15730496, upload-time = "2026-05-18T23:36:25.713Z" },
+ { url = "https://files.pythonhosted.org/packages/51/e7/38d3ea825dcab85a591734decb2f6c67caa7c8367d374df1a1c3842f9b07/numpy-2.4.6-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7d92c3819208a60205a12a245c91ad70cb0a85336659b19b834205573ac8456e", size = 16679616, upload-time = "2026-05-18T23:36:29.652Z" },
+ { url = "https://files.pythonhosted.org/packages/93/b7/caabfdf53edf663e0b4eb74d7d405d83baef09eb5e83bcd32d601d72b93e/numpy-2.4.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e85b752a1e912b70eaad4fafbd4d1238007ab221de2009b9a2f5ae7461239895", size = 17085145, upload-time = "2026-05-18T23:36:33.449Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/45/68d7c33a6bcf3e5aa3bdbd57a367e6f615286dfd6482f97e8ffeb734306e/numpy-2.4.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:29cb7f67d10b479ff07c17d33e39f78c07f71c40ef30d63c153d340e96cd3fb4", size = 18403813, upload-time = "2026-05-18T23:36:37.369Z" },
+ { url = "https://files.pythonhosted.org/packages/9c/50/0753655aa844c99cd9e018aacf76f130f1bd81d881bb74bc0aef5d73a8ba/numpy-2.4.6-cp314-cp314t-win32.whl", hash = "sha256:260a5d70215b61ab4fadf5c7baacd64821842975eea312125ed3c39a6391b063", size = 6156982, upload-time = "2026-05-18T23:36:40.817Z" },
+ { url = "https://files.pythonhosted.org/packages/b2/d4/7c67becf668f973cb490cec3e98dfd799d866f9c989a54d355672cfa0db6/numpy-2.4.6-cp314-cp314t-win_amd64.whl", hash = "sha256:81a1cca95ed5bb92aa8b10dd2cdc9a0d3853a50fad926c28b5d7e8ea54389627", size = 12638908, upload-time = "2026-05-18T23:36:43.996Z" },
+ { url = "https://files.pythonhosted.org/packages/43/bb/e1c71a4295b1b1d1393d50dbb4f2a36283c6859d9d3892e84f00ec5a91d5/numpy-2.4.6-cp314-cp314t-win_arm64.whl", hash = "sha256:0c9136e14ed34a9e343a31c533d78a9813a69a3148332bce5e9821cb2f996e66", size = 10565867, upload-time = "2026-05-18T23:36:47.114Z" },
]
[[package]]
@@ -754,36 +755,36 @@ version = "3.11.9"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/7e/0c/964746fcafbd16f8ff53219ad9f6b412b34f345c75f384ad434ceaadb538/orjson-3.11.9.tar.gz", hash = "sha256:4fef17e1f8722c11587a6ef18e35902450221da0028e65dbaaa543619e68e48f", size = 5599163, upload-time = "2026-05-06T15:11:08.309Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/32/33/93fcc25907235c344ae73122f8a4e01d2d393ef062b4af7d2e2487a32c37/orjson-3.11.9-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:4bab1b2d6141fe7b32ae71dac905666ece4f94936efbfb13d55bb7739a3a6021", size = 228458, upload-time = "2026-05-06T15:10:20.079Z" },
- { url = "https://files.pythonhosted.org/packages/8f/27/b1e6dadb3c080313c03fdd8067b85e6a0460c7d8d6a1c3984ef77b904e4d/orjson-3.11.9-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:844417969855fc7a41be124aafe83dc424592a7f77cd4501900c67307122b92c", size = 128368, upload-time = "2026-05-06T15:10:21.549Z" },
- { url = "https://files.pythonhosted.org/packages/21/0f/c9ede0bf052f6b4051e64a7d4fa91b725cccf8321a6a786e86eb03519f00/orjson-3.11.9-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ffe02797b5e9f3a9d8292ddcd289b474ad13e81ad83cd1891a240811f1d2cb81", size = 132070, upload-time = "2026-05-06T15:10:23.371Z" },
- { url = "https://files.pythonhosted.org/packages/fd/26/d398e28048dc18205bbe812f2c88cb9b40313db2470778e25964796458fe/orjson-3.11.9-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0e4eed3b200023042814d2fc8a5d2e880f13b52e1ed2485e83da4f3962f7dc1a", size = 127892, upload-time = "2026-05-06T15:10:24.714Z" },
- { url = "https://files.pythonhosted.org/packages/66/60/52b0054c4c700d5aa7fc5b7ca96917400d8f061307778578e67a10e25852/orjson-3.11.9-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8aff7da9952a5ad1cef8e68017724d96c7b9a66e99e91d6252e1b133d67a7b10", size = 135217, upload-time = "2026-05-06T15:10:26.084Z" },
- { url = "https://files.pythonhosted.org/packages/d5/97/1e3dc2b2a28b7b2528f403d2fc1d79ec5f39af3bc143ab65d3ec26426385/orjson-3.11.9-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4d4e98d6f3b8afed8bc8cd9718ec0cdf46661826beefb53fe8eafb37f2bf0362", size = 145980, upload-time = "2026-05-06T15:10:28.062Z" },
- { url = "https://files.pythonhosted.org/packages/fc/39/31fbfe7850f2de32dee7e7e5c09f26d403ab01e440ac96001c6b01ad3c99/orjson-3.11.9-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3a81d52442a7c99b3662333235b3adf96a1715864658b35bb797212be7bddb97", size = 132738, upload-time = "2026-05-06T15:10:29.727Z" },
- { url = "https://files.pythonhosted.org/packages/a1/08/dca0082dd2a194acb93e5457e73455388e2e2ca464a2672449a9ddbb679d/orjson-3.11.9-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e39364e726a8fff737309aff059ff67d8a8c8d5b677be7bb49a8b3e84b7e218", size = 134033, upload-time = "2026-05-06T15:10:31.152Z" },
- { url = "https://files.pythonhosted.org/packages/11/d4/5bdb0626801230139987385554c5d4c42255218ac906525bf4347f22cd95/orjson-3.11.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4fd66214623f1b17501df9f0543bef0b833979ab5b6ded1e1d123222866aa8c9", size = 141492, upload-time = "2026-05-06T15:10:32.641Z" },
- { url = "https://files.pythonhosted.org/packages/fa/88/a21fb53b3ede6703aede6dce4710ed4111e5b201cfa6bbff5e544f9d47d7/orjson-3.11.9-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:8ecc30f10465fa1e0ce13fd01d9e22c316e5053a719a8d915d4545a09a5ff677", size = 415087, upload-time = "2026-05-06T15:10:34.438Z" },
- { url = "https://files.pythonhosted.org/packages/3d/57/1b30daf70f0d8180e9a73cefbfbdd99e4bf19eb020466502b01fba7e0e50/orjson-3.11.9-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:97db4c94a7db398a5bd636273324f0b3fd58b350bbbac8bb380ceb825a9b40f4", size = 148031, upload-time = "2026-05-06T15:10:36.358Z" },
- { url = "https://files.pythonhosted.org/packages/04/83/45fbb6d962e260807f99441db9613cee868ceda4baceda59b3720a563f97/orjson-3.11.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9f78cf8fec5bd627f4082b8dfeac7871b43d7f3274904492a43dab39f18a19a0", size = 136915, upload-time = "2026-05-06T15:10:38.013Z" },
- { url = "https://files.pythonhosted.org/packages/5f/cc/2d10025f9056d376e4127ec05a5808b218d46f035fdc08178a5411b34250/orjson-3.11.9-cp313-cp313-win32.whl", hash = "sha256:d4087e5c0209a0a8efe4de3303c234b9c44d1174161dcd851e8eea07c7560b32", size = 131613, upload-time = "2026-05-06T15:10:39.569Z" },
- { url = "https://files.pythonhosted.org/packages/67/bd/2775ff28bfe883b9aa1ff348300542eb2ef1ee18d8ae0e3a49846817a865/orjson-3.11.9-cp313-cp313-win_amd64.whl", hash = "sha256:051b102c93b4f634e89f3866b07b9a9a98915ada541f4ec30f177067b2694979", size = 127086, upload-time = "2026-05-06T15:10:41.262Z" },
- { url = "https://files.pythonhosted.org/packages/91/2b/d26799e580939e32a7da9a39531bc9e58e15ca32ffaa6a8cb3e9bb0d22cd/orjson-3.11.9-cp313-cp313-win_arm64.whl", hash = "sha256:cce9127885941bd28f080cecf1f1d288336b7e0d812c345b08be88b572796254", size = 126696, upload-time = "2026-05-06T15:10:42.651Z" },
- { url = "https://files.pythonhosted.org/packages/8e/eb/5da01e356015aee6ecfa1187ced87aef51364e306f5e695dd52719bf0e78/orjson-3.11.9-cp314-cp314-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:b6ef1979adc4bc243523f1a2ba91418030a8e29b0a99cbe7e0e2d6807d4dce6e", size = 228465, upload-time = "2026-05-06T15:10:44.097Z" },
- { url = "https://files.pythonhosted.org/packages/64/62/3e0e0c14c957133bcd855395c62b55ed4e3b0af23ffea11b032cb1dcbdb1/orjson-3.11.9-cp314-cp314-macosx_15_0_arm64.whl", hash = "sha256:f36b7f32c7c0db4a719f1fc5824db4a9c6f8bd1a354debb91faf26ebf3a4c71e", size = 128364, upload-time = "2026-05-06T15:10:45.839Z" },
- { url = "https://files.pythonhosted.org/packages/5a/5a/07d8aa117211a8ed7630bda80c8c0b14d04e0f8dcf99bcf49656e4a710eb/orjson-3.11.9-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:08f4d8ebb44925c794e535b2bebc507cebf32209df81de22ae285fb0d8d66de0", size = 132063, upload-time = "2026-05-06T15:10:47.267Z" },
- { url = "https://files.pythonhosted.org/packages/d6/ec/4acaf21483e18aa945be74a474c74b434f284b549f275a0a39b9f98956e9/orjson-3.11.9-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6cc7923789694fd58f001cbcac7e47abc13af4d560ebbfcf3b41a8b1a0748124", size = 122356, upload-time = "2026-05-06T15:10:48.765Z" },
- { url = "https://files.pythonhosted.org/packages/13/d8/5f0555e7638801323b7a75850f92e7dfa891bc84fe27a1ba4449170d1200/orjson-3.11.9-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea5c46eb2d3af39e806b986f4b09d5c2706a1f5afde3cbf7544ce6616127173c", size = 129592, upload-time = "2026-05-06T15:10:50.13Z" },
- { url = "https://files.pythonhosted.org/packages/b6/30/ed9860412a3603ceb3c5955bfd72d28b9d0e7ba6ed81add14f83d7114236/orjson-3.11.9-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f5d89a2ed90731df3be64bab0aa44f78bff39fdc9d71c291f4a8023aa46425b7", size = 140491, upload-time = "2026-05-06T15:10:51.582Z" },
- { url = "https://files.pythonhosted.org/packages/d0/17/adc514dea7ac7c505527febf884934b815d34f0c7b8693c1a8b39c5c4a57/orjson-3.11.9-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:25e4aed0312d292c09f61af25bba34e0b2c88546041472b09088c39a4d828af1", size = 127309, upload-time = "2026-05-06T15:10:53.329Z" },
- { url = "https://files.pythonhosted.org/packages/76/3e/c0b690253f0b82d86e99949af13533363acfb5432ecb5d53dd5b3bce9c34/orjson-3.11.9-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aaea64f3f467d22e70eeed68bdccb3bc4f83f650446c4a03c59f2cba28a108db", size = 134030, upload-time = "2026-05-06T15:10:54.988Z" },
- { url = "https://files.pythonhosted.org/packages/c1/7a/bc82a0bb25e9faaf92dc4d9ef002732efc09737706af83e346788641d4a7/orjson-3.11.9-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a028425d1b440c5d92a6be1e1a020739dfe67ea87d96c6dbe828c1b30041728b", size = 141482, upload-time = "2026-05-06T15:10:56.663Z" },
- { url = "https://files.pythonhosted.org/packages/01/55/e69188b939f77d5d32a9833745ace31ea5ccae3ab613a1ec185d3cd2c4fb/orjson-3.11.9-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:5b192c6cf397e4455b11523c5cf2b18ed084c1bbd61b6c0926344d2129481972", size = 415178, upload-time = "2026-05-06T15:10:58.446Z" },
- { url = "https://files.pythonhosted.org/packages/2e/1a/b8a5a7ac527e80b9cb11d51e3f6689b709279183264b9ec5c7bc680bb8b5/orjson-3.11.9-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ea407d4ccf5891d667d045fecae97a7a1e5e87b3b97f97ae1803c2e741130be0", size = 148089, upload-time = "2026-05-06T15:11:00.441Z" },
- { url = "https://files.pythonhosted.org/packages/97/4e/00503f64204bf859b37213a63927028f30fb6268cd8677fb0a5ad48155e1/orjson-3.11.9-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5f63aaf97afd9f6dec5b1a68e1b8da12bfccb4cb9a9a65c3e0b6c847849e7586", size = 136921, upload-time = "2026-05-06T15:11:02.176Z" },
- { url = "https://files.pythonhosted.org/packages/0d/ba/a23b82a0a8d0ed7bed4e5f5035aae751cad4ff6a1e8d2ecd14d8860f5929/orjson-3.11.9-cp314-cp314-win32.whl", hash = "sha256:e30ab17845bb9fa54ccf67fa4f9f5282652d54faa6d17452f47d0f369d038673", size = 131638, upload-time = "2026-05-06T15:11:03.696Z" },
- { url = "https://files.pythonhosted.org/packages/f3/c3/0c6798456bade745c75c452342dabacce5798196483e77e643be1f53877d/orjson-3.11.9-cp314-cp314-win_amd64.whl", hash = "sha256:32ef5f4283a3be81913947d19608eacb7c6608026851123790cd9cc8982af34b", size = 127078, upload-time = "2026-05-06T15:11:05.123Z" },
- { url = "https://files.pythonhosted.org/packages/16/21/5a3f1e8913103b703a436a5664238e5b965ec392b555fe68943ea3691e6b/orjson-3.11.9-cp314-cp314-win_arm64.whl", hash = "sha256:eebdbdeef0094e4f5aefa20dcd4eb2368ab5e7a3b4edea27f1e7b2892e009cf9", size = 126687, upload-time = "2026-05-06T15:11:06.602Z" },
+ { url = "https://files.pythonhosted.org/packages/32/33/93fcc25907235c344ae73122f8a4e01d2d393ef062b4af7d2e2487a32c37/orjson-3.11.9-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:4bab1b2d6141fe7b32ae71dac905666ece4f94936efbfb13d55bb7739a3a6021", size = 228458, upload-time = "2026-05-06T15:10:20.079Z" },
+ { url = "https://files.pythonhosted.org/packages/8f/27/b1e6dadb3c080313c03fdd8067b85e6a0460c7d8d6a1c3984ef77b904e4d/orjson-3.11.9-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:844417969855fc7a41be124aafe83dc424592a7f77cd4501900c67307122b92c", size = 128368, upload-time = "2026-05-06T15:10:21.549Z" },
+ { url = "https://files.pythonhosted.org/packages/21/0f/c9ede0bf052f6b4051e64a7d4fa91b725cccf8321a6a786e86eb03519f00/orjson-3.11.9-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ffe02797b5e9f3a9d8292ddcd289b474ad13e81ad83cd1891a240811f1d2cb81", size = 132070, upload-time = "2026-05-06T15:10:23.371Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/26/d398e28048dc18205bbe812f2c88cb9b40313db2470778e25964796458fe/orjson-3.11.9-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0e4eed3b200023042814d2fc8a5d2e880f13b52e1ed2485e83da4f3962f7dc1a", size = 127892, upload-time = "2026-05-06T15:10:24.714Z" },
+ { url = "https://files.pythonhosted.org/packages/66/60/52b0054c4c700d5aa7fc5b7ca96917400d8f061307778578e67a10e25852/orjson-3.11.9-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8aff7da9952a5ad1cef8e68017724d96c7b9a66e99e91d6252e1b133d67a7b10", size = 135217, upload-time = "2026-05-06T15:10:26.084Z" },
+ { url = "https://files.pythonhosted.org/packages/d5/97/1e3dc2b2a28b7b2528f403d2fc1d79ec5f39af3bc143ab65d3ec26426385/orjson-3.11.9-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4d4e98d6f3b8afed8bc8cd9718ec0cdf46661826beefb53fe8eafb37f2bf0362", size = 145980, upload-time = "2026-05-06T15:10:28.062Z" },
+ { url = "https://files.pythonhosted.org/packages/fc/39/31fbfe7850f2de32dee7e7e5c09f26d403ab01e440ac96001c6b01ad3c99/orjson-3.11.9-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3a81d52442a7c99b3662333235b3adf96a1715864658b35bb797212be7bddb97", size = 132738, upload-time = "2026-05-06T15:10:29.727Z" },
+ { url = "https://files.pythonhosted.org/packages/a1/08/dca0082dd2a194acb93e5457e73455388e2e2ca464a2672449a9ddbb679d/orjson-3.11.9-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e39364e726a8fff737309aff059ff67d8a8c8d5b677be7bb49a8b3e84b7e218", size = 134033, upload-time = "2026-05-06T15:10:31.152Z" },
+ { url = "https://files.pythonhosted.org/packages/11/d4/5bdb0626801230139987385554c5d4c42255218ac906525bf4347f22cd95/orjson-3.11.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4fd66214623f1b17501df9f0543bef0b833979ab5b6ded1e1d123222866aa8c9", size = 141492, upload-time = "2026-05-06T15:10:32.641Z" },
+ { url = "https://files.pythonhosted.org/packages/fa/88/a21fb53b3ede6703aede6dce4710ed4111e5b201cfa6bbff5e544f9d47d7/orjson-3.11.9-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:8ecc30f10465fa1e0ce13fd01d9e22c316e5053a719a8d915d4545a09a5ff677", size = 415087, upload-time = "2026-05-06T15:10:34.438Z" },
+ { url = "https://files.pythonhosted.org/packages/3d/57/1b30daf70f0d8180e9a73cefbfbdd99e4bf19eb020466502b01fba7e0e50/orjson-3.11.9-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:97db4c94a7db398a5bd636273324f0b3fd58b350bbbac8bb380ceb825a9b40f4", size = 148031, upload-time = "2026-05-06T15:10:36.358Z" },
+ { url = "https://files.pythonhosted.org/packages/04/83/45fbb6d962e260807f99441db9613cee868ceda4baceda59b3720a563f97/orjson-3.11.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9f78cf8fec5bd627f4082b8dfeac7871b43d7f3274904492a43dab39f18a19a0", size = 136915, upload-time = "2026-05-06T15:10:38.013Z" },
+ { url = "https://files.pythonhosted.org/packages/5f/cc/2d10025f9056d376e4127ec05a5808b218d46f035fdc08178a5411b34250/orjson-3.11.9-cp313-cp313-win32.whl", hash = "sha256:d4087e5c0209a0a8efe4de3303c234b9c44d1174161dcd851e8eea07c7560b32", size = 131613, upload-time = "2026-05-06T15:10:39.569Z" },
+ { url = "https://files.pythonhosted.org/packages/67/bd/2775ff28bfe883b9aa1ff348300542eb2ef1ee18d8ae0e3a49846817a865/orjson-3.11.9-cp313-cp313-win_amd64.whl", hash = "sha256:051b102c93b4f634e89f3866b07b9a9a98915ada541f4ec30f177067b2694979", size = 127086, upload-time = "2026-05-06T15:10:41.262Z" },
+ { url = "https://files.pythonhosted.org/packages/91/2b/d26799e580939e32a7da9a39531bc9e58e15ca32ffaa6a8cb3e9bb0d22cd/orjson-3.11.9-cp313-cp313-win_arm64.whl", hash = "sha256:cce9127885941bd28f080cecf1f1d288336b7e0d812c345b08be88b572796254", size = 126696, upload-time = "2026-05-06T15:10:42.651Z" },
+ { url = "https://files.pythonhosted.org/packages/8e/eb/5da01e356015aee6ecfa1187ced87aef51364e306f5e695dd52719bf0e78/orjson-3.11.9-cp314-cp314-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:b6ef1979adc4bc243523f1a2ba91418030a8e29b0a99cbe7e0e2d6807d4dce6e", size = 228465, upload-time = "2026-05-06T15:10:44.097Z" },
+ { url = "https://files.pythonhosted.org/packages/64/62/3e0e0c14c957133bcd855395c62b55ed4e3b0af23ffea11b032cb1dcbdb1/orjson-3.11.9-cp314-cp314-macosx_15_0_arm64.whl", hash = "sha256:f36b7f32c7c0db4a719f1fc5824db4a9c6f8bd1a354debb91faf26ebf3a4c71e", size = 128364, upload-time = "2026-05-06T15:10:45.839Z" },
+ { url = "https://files.pythonhosted.org/packages/5a/5a/07d8aa117211a8ed7630bda80c8c0b14d04e0f8dcf99bcf49656e4a710eb/orjson-3.11.9-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:08f4d8ebb44925c794e535b2bebc507cebf32209df81de22ae285fb0d8d66de0", size = 132063, upload-time = "2026-05-06T15:10:47.267Z" },
+ { url = "https://files.pythonhosted.org/packages/d6/ec/4acaf21483e18aa945be74a474c74b434f284b549f275a0a39b9f98956e9/orjson-3.11.9-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6cc7923789694fd58f001cbcac7e47abc13af4d560ebbfcf3b41a8b1a0748124", size = 122356, upload-time = "2026-05-06T15:10:48.765Z" },
+ { url = "https://files.pythonhosted.org/packages/13/d8/5f0555e7638801323b7a75850f92e7dfa891bc84fe27a1ba4449170d1200/orjson-3.11.9-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea5c46eb2d3af39e806b986f4b09d5c2706a1f5afde3cbf7544ce6616127173c", size = 129592, upload-time = "2026-05-06T15:10:50.13Z" },
+ { url = "https://files.pythonhosted.org/packages/b6/30/ed9860412a3603ceb3c5955bfd72d28b9d0e7ba6ed81add14f83d7114236/orjson-3.11.9-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f5d89a2ed90731df3be64bab0aa44f78bff39fdc9d71c291f4a8023aa46425b7", size = 140491, upload-time = "2026-05-06T15:10:51.582Z" },
+ { url = "https://files.pythonhosted.org/packages/d0/17/adc514dea7ac7c505527febf884934b815d34f0c7b8693c1a8b39c5c4a57/orjson-3.11.9-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:25e4aed0312d292c09f61af25bba34e0b2c88546041472b09088c39a4d828af1", size = 127309, upload-time = "2026-05-06T15:10:53.329Z" },
+ { url = "https://files.pythonhosted.org/packages/76/3e/c0b690253f0b82d86e99949af13533363acfb5432ecb5d53dd5b3bce9c34/orjson-3.11.9-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aaea64f3f467d22e70eeed68bdccb3bc4f83f650446c4a03c59f2cba28a108db", size = 134030, upload-time = "2026-05-06T15:10:54.988Z" },
+ { url = "https://files.pythonhosted.org/packages/c1/7a/bc82a0bb25e9faaf92dc4d9ef002732efc09737706af83e346788641d4a7/orjson-3.11.9-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a028425d1b440c5d92a6be1e1a020739dfe67ea87d96c6dbe828c1b30041728b", size = 141482, upload-time = "2026-05-06T15:10:56.663Z" },
+ { url = "https://files.pythonhosted.org/packages/01/55/e69188b939f77d5d32a9833745ace31ea5ccae3ab613a1ec185d3cd2c4fb/orjson-3.11.9-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:5b192c6cf397e4455b11523c5cf2b18ed084c1bbd61b6c0926344d2129481972", size = 415178, upload-time = "2026-05-06T15:10:58.446Z" },
+ { url = "https://files.pythonhosted.org/packages/2e/1a/b8a5a7ac527e80b9cb11d51e3f6689b709279183264b9ec5c7bc680bb8b5/orjson-3.11.9-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ea407d4ccf5891d667d045fecae97a7a1e5e87b3b97f97ae1803c2e741130be0", size = 148089, upload-time = "2026-05-06T15:11:00.441Z" },
+ { url = "https://files.pythonhosted.org/packages/97/4e/00503f64204bf859b37213a63927028f30fb6268cd8677fb0a5ad48155e1/orjson-3.11.9-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5f63aaf97afd9f6dec5b1a68e1b8da12bfccb4cb9a9a65c3e0b6c847849e7586", size = 136921, upload-time = "2026-05-06T15:11:02.176Z" },
+ { url = "https://files.pythonhosted.org/packages/0d/ba/a23b82a0a8d0ed7bed4e5f5035aae751cad4ff6a1e8d2ecd14d8860f5929/orjson-3.11.9-cp314-cp314-win32.whl", hash = "sha256:e30ab17845bb9fa54ccf67fa4f9f5282652d54faa6d17452f47d0f369d038673", size = 131638, upload-time = "2026-05-06T15:11:03.696Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/c3/0c6798456bade745c75c452342dabacce5798196483e77e643be1f53877d/orjson-3.11.9-cp314-cp314-win_amd64.whl", hash = "sha256:32ef5f4283a3be81913947d19608eacb7c6608026851123790cd9cc8982af34b", size = 127078, upload-time = "2026-05-06T15:11:05.123Z" },
+ { url = "https://files.pythonhosted.org/packages/16/21/5a3f1e8913103b703a436a5664238e5b965ec392b555fe68943ea3691e6b/orjson-3.11.9-cp314-cp314-win_arm64.whl", hash = "sha256:eebdbdeef0094e4f5aefa20dcd4eb2368ab5e7a3b4edea27f1e7b2892e009cf9", size = 126687, upload-time = "2026-05-06T15:11:06.602Z" },
]
[[package]]
@@ -792,7 +793,7 @@ version = "26.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" },
+ { url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" },
]
[[package]]
@@ -800,43 +801,43 @@ name = "pandas"
version = "3.0.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
- { name = "numpy" },
- { name = "python-dateutil" },
- { name = "tzdata", marker = "sys_platform == 'emscripten' or sys_platform == 'win32'" },
+ { name = "numpy" },
+ { name = "python-dateutil" },
+ { name = "tzdata", marker = "sys_platform == 'emscripten' or sys_platform == 'win32'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/f8/87/4341c6252d1c47b08768c3d25ac487362bf403f0313ddae4a2a26c9b1b4c/pandas-3.0.3.tar.gz", hash = "sha256:696a4a00a2a2a35d4e5deb3fc946641b96c944f02230e4f76137fe35d806c4fc", size = 4651414, upload-time = "2026-05-11T18:54:29.21Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/c5/90/62d8302883c44308c477e222c3daf7c813a34c8e96985882fbd53d964352/pandas-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:67b3b64c11910cfa29f4e94a14d3bff9ee693b6fc76055e7cad549cee0aec5fa", size = 10331071, upload-time = "2026-05-11T18:52:58.838Z" },
- { url = "https://files.pythonhosted.org/packages/7f/ae/6a6493c783a101f165e4356953ba3c74d6f77f0042fa7d753da9dfbb640c/pandas-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:39436b377d56d2a2e52d0395bdbee171f01068e99af5250509aceeb929f765c7", size = 9875690, upload-time = "2026-05-11T18:53:01.431Z" },
- { url = "https://files.pythonhosted.org/packages/62/7c/5df8e9f56c69a2769fbe9382a5ef8f2658c007e376434e1e2cbb57ad895f/pandas-3.0.3-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d4be06d68f9ddcfc645b87534911da79a8fbffc7573c80e0edcf42a5020624d8", size = 10381634, upload-time = "2026-05-11T18:53:04.393Z" },
- { url = "https://files.pythonhosted.org/packages/99/68/1237369725aa617bb358263d535803e3053fdbc593513ec5ed9c9896b5b6/pandas-3.0.3-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a4eeb6830daf35a71cc09649bd823e2b542dac246cdee9614c6e4bd65028cd6a", size = 10891243, upload-time = "2026-05-11T18:53:07.643Z" },
- { url = "https://files.pythonhosted.org/packages/25/93/77d108e8af7222b4a503ebde0e30215b1c2e4f8e53a526431890f22d5586/pandas-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1928e07221f82db493cd4af1e23c1bfca524a19a4699887975bff68f49a72bfb", size = 11388659, upload-time = "2026-05-11T18:53:10.634Z" },
- { url = "https://files.pythonhosted.org/packages/d0/bd/eff5b4399f332ac386c853f6cd2bd3fa2ca0061b9f36ecd9c4d7c4265649/pandas-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51b1fe551acb77dac643c6fda86084d8d446c10fe64b06a9cc29c4cc8540e7f2", size = 11942880, upload-time = "2026-05-11T18:53:13.536Z" },
- { url = "https://files.pythonhosted.org/packages/2c/20/559ace4200982c3887d0b86bfd0d856a2143ef8ddab63cc07934951a964c/pandas-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:a82d532a3351d435432cd913edbccaf8b8e01d4dd0e5ced5a8d2e8ecd94c7e44", size = 9757091, upload-time = "2026-05-11T18:53:16.306Z" },
- { url = "https://files.pythonhosted.org/packages/3a/66/69055a09fe200f29f922a3eeec4804611900b95f52d932ece3393c3c0c19/pandas-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:275c14e0fce14a2ec20eee474aecd305478ea3c1e6f6a9d8fe219a165542717e", size = 9057282, upload-time = "2026-05-11T18:53:18.768Z" },
- { url = "https://files.pythonhosted.org/packages/57/0e/efe801b0e6811e8e650cd21b7f2608e30f08a7067e2bf6e8752b0d56ee3c/pandas-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:46997386d528eb40376ecd6b033cf4a8a1e5282580f68f43de875b78cba2199d", size = 10767016, upload-time = "2026-05-11T18:53:21.227Z" },
- { url = "https://files.pythonhosted.org/packages/ea/dc/eb55135a1d5f0f0519f28da1f609a206d2cad1f9c35c32d51e38dd7261ae/pandas-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:261e308dfb22448384b7580cf719d2f998fe2966c92893c3e77d14008af1f066", size = 10420210, upload-time = "2026-05-11T18:53:23.982Z" },
- { url = "https://files.pythonhosted.org/packages/c6/3e/b1d5d955ce33ffecb407465a60bc32769d74fcf68224b7ae67ae11d4dea4/pandas-3.0.3-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dd1a5d1def6a46002e964510bdc67c368aa0951df5d1d9f8365336f5a1f490cd", size = 10336126, upload-time = "2026-05-11T18:53:26.731Z" },
- { url = "https://files.pythonhosted.org/packages/f5/76/a01261711ab60a22d71b862f0de20e4c504bf80457270ad8cb42110f6abc/pandas-3.0.3-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d72828c20c6d6e83e1e22a6a3b47b326b71664112fa9705dcbccfd7a39b62085", size = 10728051, upload-time = "2026-05-11T18:53:29.125Z" },
- { url = "https://files.pythonhosted.org/packages/e9/21/ea191195e587b18cf682e97f433f81b2d0fbe341380e80a3e0d6e4403c8e/pandas-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:d26cbe1fcfc12e8fd900e2454163e466b2d3af84f7c75481df7683ffc073d870", size = 11350796, upload-time = "2026-05-11T18:53:32.056Z" },
- { url = "https://files.pythonhosted.org/packages/64/69/f0eaaf54939f0e8c6768fd06be9af2cef9b36048b96dfb9e1b2c685a807e/pandas-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:3e91cec1879ada0624fc3dc9953c5cbd60208e59c0db28f540c5d6d47502422f", size = 11799741, upload-time = "2026-05-11T18:53:34.985Z" },
- { url = "https://files.pythonhosted.org/packages/45/a4/865e0e510cae5fc2194de4db28be638952de942571ba9125934fd9c01d47/pandas-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:08d789b41f87e0905880e293cedf6197ce71fe67cc081358b1e148a491b9bd13", size = 10499958, upload-time = "2026-05-11T18:53:37.857Z" },
- { url = "https://files.pythonhosted.org/packages/86/54/effdcc3c0ff7a08037889200e148ebe94c16c4f653be078c7b3675955df1/pandas-3.0.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:3650109c0f22879df8bd6179ab9ee3d7f1d1d4e7e0094a3f0032d9f51e2e64ac", size = 10336065, upload-time = "2026-05-11T18:53:41.099Z" },
- { url = "https://files.pythonhosted.org/packages/68/10/bf2d6738d72748b961a3751ab89522d58c54efc36a8e1a12161216cd45cf/pandas-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:bab900348131a7db1f69a7309ef141fd5680f1487094193bcbbb61791573bf8f", size = 9926101, upload-time = "2026-05-11T18:53:43.515Z" },
- { url = "https://files.pythonhosted.org/packages/ae/e9/e35cf11c8a136e757b956f5f0efdcaa50aecde85ea055f1898dfc68262f3/pandas-3.0.3-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ba7e08b9ac1d54569cd1e256e3668975ed624d6826f7b68df0342b012007bddb", size = 10457553, upload-time = "2026-05-11T18:53:46.394Z" },
- { url = "https://files.pythonhosted.org/packages/58/3b/1cdec6772bdbaf7b25dab360c59f03cadf05492dd724c6540af905389b07/pandas-3.0.3-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d71c63ae4ebdbf70209742096f1fc46a83a0613c99d4b23766cced9ff8cd62a", size = 10914065, upload-time = "2026-05-11T18:53:49.134Z" },
- { url = "https://files.pythonhosted.org/packages/c4/c2/1ef644445fcd72e3627bceec77e3560636f87ddce4ed841afe76b83b5bf9/pandas-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e3a2ec42c98ffa2565a67e08e218d06d72576d758d90facb7c00805194d8f360", size = 11459188, upload-time = "2026-05-11T18:53:52.527Z" },
- { url = "https://files.pythonhosted.org/packages/7e/49/4d8d4f42cbc9c4adc7a1870f269c02cbd6cd40d059622c06fb298addcbad/pandas-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:335f62418ed562cfc3c49e9e196375c28b729dcef8543abf4f9438e381bf3c76", size = 11982966, upload-time = "2026-05-11T18:53:55.043Z" },
- { url = "https://files.pythonhosted.org/packages/38/55/792619469bab9882d8bbd5865d45a72f6478762d04a9af4bf0d08c503e95/pandas-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:3c20a521bbb85902f79f7270c80a59e1b5452d96d170c034f207181870f97ac5", size = 9876755, upload-time = "2026-05-11T18:53:58.067Z" },
- { url = "https://files.pythonhosted.org/packages/2a/af/33c469653b0ba03b50c3a98192d4c07f0c75c66b263ceb097fce0ee97d31/pandas-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:a2d2dff8a04f3917b55ab3910c32990f8ddf7eceba114947838cefa976a68977", size = 9198658, upload-time = "2026-05-11T18:54:00.733Z" },
- { url = "https://files.pythonhosted.org/packages/a2/fa/b8c257bd76b8bd060c3a9151c1fca05e9b9c5e3af5d0f549c0356f6d143d/pandas-3.0.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:0d589105b3c14645af1738ff279b2995102d8f7a03b0a66dc8d95550eb513e04", size = 10787242, upload-time = "2026-05-11T18:54:03.564Z" },
- { url = "https://files.pythonhosted.org/packages/54/eb/f19206ffb0bf1919002969aa448b4702c6594845156a6f8050674855aac3/pandas-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:13fc1e853d9e04743d11ba75a985ccbc2a317fe07d8af61e445a6fd24dacd6a6", size = 10436369, upload-time = "2026-05-11T18:54:06.311Z" },
- { url = "https://files.pythonhosted.org/packages/fd/24/c7c39fb4fe22b71a0c2d78bf0c585c600092d85f94f086d2b3b2f6ca27e2/pandas-3.0.3-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:819959dab7bbd0049c15623fbac4e29a191b9528160a61fb1032242d8ced2d9c", size = 10358306, upload-time = "2026-05-11T18:54:09.085Z" },
- { url = "https://files.pythonhosted.org/packages/16/ec/dd2a9eb7fa1204df88c0864164e35b228ac581062ac612ba0a67fd812e4c/pandas-3.0.3-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:60ae316d3fd75d1858d450d0db0103ea2be3e7d4a95ec2f064f7e2ae63f7b028", size = 10758394, upload-time = "2026-05-11T18:54:11.956Z" },
- { url = "https://files.pythonhosted.org/packages/95/6e/00c61ea8e85b4f6d8d35e11852a1a4998fc7fafc91c6a602d1cc9c972d64/pandas-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bd3a518890b400d32f9023722dc9a9a5c969f00b415419a3c06c043f09bb5d7d", size = 11375717, upload-time = "2026-05-11T18:54:14.539Z" },
- { url = "https://files.pythonhosted.org/packages/31/89/8fc1c268969fac43688d65fd92e67df24bd128d53cb4d2eee534cd307399/pandas-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9c39be2d709d01fa972a0cabc522389fceca4f3969332ba25a7d6c5802cf976a", size = 11828897, upload-time = "2026-05-11T18:54:17.146Z" },
- { url = "https://files.pythonhosted.org/packages/56/3b/e7d20dea247a3e6dc0bd8a6953854afbedc03951def4e7371e05e7263e25/pandas-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4db8c527972a821cf5286b40ccc57642a39bc62e62022b42f99f8a67fca8c3a1", size = 10900855, upload-time = "2026-05-11T18:54:19.72Z" },
- { url = "https://files.pythonhosted.org/packages/0f/54/68a0978d1ef8502b8492099beaa6e7a0c1b32e3b5d4f677f5810cb08711c/pandas-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:b2c95f8bfc1ee412bf482605d7bfd30c12d1d26bd59fdd91efeef1d4718decb1", size = 9466464, upload-time = "2026-05-11T18:54:22.754Z" },
+ { url = "https://files.pythonhosted.org/packages/c5/90/62d8302883c44308c477e222c3daf7c813a34c8e96985882fbd53d964352/pandas-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:67b3b64c11910cfa29f4e94a14d3bff9ee693b6fc76055e7cad549cee0aec5fa", size = 10331071, upload-time = "2026-05-11T18:52:58.838Z" },
+ { url = "https://files.pythonhosted.org/packages/7f/ae/6a6493c783a101f165e4356953ba3c74d6f77f0042fa7d753da9dfbb640c/pandas-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:39436b377d56d2a2e52d0395bdbee171f01068e99af5250509aceeb929f765c7", size = 9875690, upload-time = "2026-05-11T18:53:01.431Z" },
+ { url = "https://files.pythonhosted.org/packages/62/7c/5df8e9f56c69a2769fbe9382a5ef8f2658c007e376434e1e2cbb57ad895f/pandas-3.0.3-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d4be06d68f9ddcfc645b87534911da79a8fbffc7573c80e0edcf42a5020624d8", size = 10381634, upload-time = "2026-05-11T18:53:04.393Z" },
+ { url = "https://files.pythonhosted.org/packages/99/68/1237369725aa617bb358263d535803e3053fdbc593513ec5ed9c9896b5b6/pandas-3.0.3-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a4eeb6830daf35a71cc09649bd823e2b542dac246cdee9614c6e4bd65028cd6a", size = 10891243, upload-time = "2026-05-11T18:53:07.643Z" },
+ { url = "https://files.pythonhosted.org/packages/25/93/77d108e8af7222b4a503ebde0e30215b1c2e4f8e53a526431890f22d5586/pandas-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1928e07221f82db493cd4af1e23c1bfca524a19a4699887975bff68f49a72bfb", size = 11388659, upload-time = "2026-05-11T18:53:10.634Z" },
+ { url = "https://files.pythonhosted.org/packages/d0/bd/eff5b4399f332ac386c853f6cd2bd3fa2ca0061b9f36ecd9c4d7c4265649/pandas-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51b1fe551acb77dac643c6fda86084d8d446c10fe64b06a9cc29c4cc8540e7f2", size = 11942880, upload-time = "2026-05-11T18:53:13.536Z" },
+ { url = "https://files.pythonhosted.org/packages/2c/20/559ace4200982c3887d0b86bfd0d856a2143ef8ddab63cc07934951a964c/pandas-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:a82d532a3351d435432cd913edbccaf8b8e01d4dd0e5ced5a8d2e8ecd94c7e44", size = 9757091, upload-time = "2026-05-11T18:53:16.306Z" },
+ { url = "https://files.pythonhosted.org/packages/3a/66/69055a09fe200f29f922a3eeec4804611900b95f52d932ece3393c3c0c19/pandas-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:275c14e0fce14a2ec20eee474aecd305478ea3c1e6f6a9d8fe219a165542717e", size = 9057282, upload-time = "2026-05-11T18:53:18.768Z" },
+ { url = "https://files.pythonhosted.org/packages/57/0e/efe801b0e6811e8e650cd21b7f2608e30f08a7067e2bf6e8752b0d56ee3c/pandas-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:46997386d528eb40376ecd6b033cf4a8a1e5282580f68f43de875b78cba2199d", size = 10767016, upload-time = "2026-05-11T18:53:21.227Z" },
+ { url = "https://files.pythonhosted.org/packages/ea/dc/eb55135a1d5f0f0519f28da1f609a206d2cad1f9c35c32d51e38dd7261ae/pandas-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:261e308dfb22448384b7580cf719d2f998fe2966c92893c3e77d14008af1f066", size = 10420210, upload-time = "2026-05-11T18:53:23.982Z" },
+ { url = "https://files.pythonhosted.org/packages/c6/3e/b1d5d955ce33ffecb407465a60bc32769d74fcf68224b7ae67ae11d4dea4/pandas-3.0.3-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dd1a5d1def6a46002e964510bdc67c368aa0951df5d1d9f8365336f5a1f490cd", size = 10336126, upload-time = "2026-05-11T18:53:26.731Z" },
+ { url = "https://files.pythonhosted.org/packages/f5/76/a01261711ab60a22d71b862f0de20e4c504bf80457270ad8cb42110f6abc/pandas-3.0.3-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d72828c20c6d6e83e1e22a6a3b47b326b71664112fa9705dcbccfd7a39b62085", size = 10728051, upload-time = "2026-05-11T18:53:29.125Z" },
+ { url = "https://files.pythonhosted.org/packages/e9/21/ea191195e587b18cf682e97f433f81b2d0fbe341380e80a3e0d6e4403c8e/pandas-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:d26cbe1fcfc12e8fd900e2454163e466b2d3af84f7c75481df7683ffc073d870", size = 11350796, upload-time = "2026-05-11T18:53:32.056Z" },
+ { url = "https://files.pythonhosted.org/packages/64/69/f0eaaf54939f0e8c6768fd06be9af2cef9b36048b96dfb9e1b2c685a807e/pandas-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:3e91cec1879ada0624fc3dc9953c5cbd60208e59c0db28f540c5d6d47502422f", size = 11799741, upload-time = "2026-05-11T18:53:34.985Z" },
+ { url = "https://files.pythonhosted.org/packages/45/a4/865e0e510cae5fc2194de4db28be638952de942571ba9125934fd9c01d47/pandas-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:08d789b41f87e0905880e293cedf6197ce71fe67cc081358b1e148a491b9bd13", size = 10499958, upload-time = "2026-05-11T18:53:37.857Z" },
+ { url = "https://files.pythonhosted.org/packages/86/54/effdcc3c0ff7a08037889200e148ebe94c16c4f653be078c7b3675955df1/pandas-3.0.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:3650109c0f22879df8bd6179ab9ee3d7f1d1d4e7e0094a3f0032d9f51e2e64ac", size = 10336065, upload-time = "2026-05-11T18:53:41.099Z" },
+ { url = "https://files.pythonhosted.org/packages/68/10/bf2d6738d72748b961a3751ab89522d58c54efc36a8e1a12161216cd45cf/pandas-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:bab900348131a7db1f69a7309ef141fd5680f1487094193bcbbb61791573bf8f", size = 9926101, upload-time = "2026-05-11T18:53:43.515Z" },
+ { url = "https://files.pythonhosted.org/packages/ae/e9/e35cf11c8a136e757b956f5f0efdcaa50aecde85ea055f1898dfc68262f3/pandas-3.0.3-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ba7e08b9ac1d54569cd1e256e3668975ed624d6826f7b68df0342b012007bddb", size = 10457553, upload-time = "2026-05-11T18:53:46.394Z" },
+ { url = "https://files.pythonhosted.org/packages/58/3b/1cdec6772bdbaf7b25dab360c59f03cadf05492dd724c6540af905389b07/pandas-3.0.3-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d71c63ae4ebdbf70209742096f1fc46a83a0613c99d4b23766cced9ff8cd62a", size = 10914065, upload-time = "2026-05-11T18:53:49.134Z" },
+ { url = "https://files.pythonhosted.org/packages/c4/c2/1ef644445fcd72e3627bceec77e3560636f87ddce4ed841afe76b83b5bf9/pandas-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e3a2ec42c98ffa2565a67e08e218d06d72576d758d90facb7c00805194d8f360", size = 11459188, upload-time = "2026-05-11T18:53:52.527Z" },
+ { url = "https://files.pythonhosted.org/packages/7e/49/4d8d4f42cbc9c4adc7a1870f269c02cbd6cd40d059622c06fb298addcbad/pandas-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:335f62418ed562cfc3c49e9e196375c28b729dcef8543abf4f9438e381bf3c76", size = 11982966, upload-time = "2026-05-11T18:53:55.043Z" },
+ { url = "https://files.pythonhosted.org/packages/38/55/792619469bab9882d8bbd5865d45a72f6478762d04a9af4bf0d08c503e95/pandas-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:3c20a521bbb85902f79f7270c80a59e1b5452d96d170c034f207181870f97ac5", size = 9876755, upload-time = "2026-05-11T18:53:58.067Z" },
+ { url = "https://files.pythonhosted.org/packages/2a/af/33c469653b0ba03b50c3a98192d4c07f0c75c66b263ceb097fce0ee97d31/pandas-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:a2d2dff8a04f3917b55ab3910c32990f8ddf7eceba114947838cefa976a68977", size = 9198658, upload-time = "2026-05-11T18:54:00.733Z" },
+ { url = "https://files.pythonhosted.org/packages/a2/fa/b8c257bd76b8bd060c3a9151c1fca05e9b9c5e3af5d0f549c0356f6d143d/pandas-3.0.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:0d589105b3c14645af1738ff279b2995102d8f7a03b0a66dc8d95550eb513e04", size = 10787242, upload-time = "2026-05-11T18:54:03.564Z" },
+ { url = "https://files.pythonhosted.org/packages/54/eb/f19206ffb0bf1919002969aa448b4702c6594845156a6f8050674855aac3/pandas-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:13fc1e853d9e04743d11ba75a985ccbc2a317fe07d8af61e445a6fd24dacd6a6", size = 10436369, upload-time = "2026-05-11T18:54:06.311Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/24/c7c39fb4fe22b71a0c2d78bf0c585c600092d85f94f086d2b3b2f6ca27e2/pandas-3.0.3-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:819959dab7bbd0049c15623fbac4e29a191b9528160a61fb1032242d8ced2d9c", size = 10358306, upload-time = "2026-05-11T18:54:09.085Z" },
+ { url = "https://files.pythonhosted.org/packages/16/ec/dd2a9eb7fa1204df88c0864164e35b228ac581062ac612ba0a67fd812e4c/pandas-3.0.3-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:60ae316d3fd75d1858d450d0db0103ea2be3e7d4a95ec2f064f7e2ae63f7b028", size = 10758394, upload-time = "2026-05-11T18:54:11.956Z" },
+ { url = "https://files.pythonhosted.org/packages/95/6e/00c61ea8e85b4f6d8d35e11852a1a4998fc7fafc91c6a602d1cc9c972d64/pandas-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bd3a518890b400d32f9023722dc9a9a5c969f00b415419a3c06c043f09bb5d7d", size = 11375717, upload-time = "2026-05-11T18:54:14.539Z" },
+ { url = "https://files.pythonhosted.org/packages/31/89/8fc1c268969fac43688d65fd92e67df24bd128d53cb4d2eee534cd307399/pandas-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9c39be2d709d01fa972a0cabc522389fceca4f3969332ba25a7d6c5802cf976a", size = 11828897, upload-time = "2026-05-11T18:54:17.146Z" },
+ { url = "https://files.pythonhosted.org/packages/56/3b/e7d20dea247a3e6dc0bd8a6953854afbedc03951def4e7371e05e7263e25/pandas-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4db8c527972a821cf5286b40ccc57642a39bc62e62022b42f99f8a67fca8c3a1", size = 10900855, upload-time = "2026-05-11T18:54:19.72Z" },
+ { url = "https://files.pythonhosted.org/packages/0f/54/68a0978d1ef8502b8492099beaa6e7a0c1b32e3b5d4f677f5810cb08711c/pandas-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:b2c95f8bfc1ee412bf482605d7bfd30c12d1d26bd59fdd91efeef1d4718decb1", size = 9466464, upload-time = "2026-05-11T18:54:22.754Z" },
]
[[package]]
@@ -845,7 +846,7 @@ version = "4.10.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d7/47/e4501f49c178ae1d9f4a75073fda4204f52647993f075a9db4d14930e0c5/platformdirs-4.10.0.tar.gz", hash = "sha256:31e761a6a0ca04faf7353ea759bdba55652be214725111e5aac52dfa29d4bef7", size = 31224, upload-time = "2026-05-28T03:32:53.587Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/81/e6/cd9575ac904136b3cbf7aa7ee819ef86eedb7274e46f230e94ea4342e729/platformdirs-4.10.0-py3-none-any.whl", hash = "sha256:fb516cdb12eb0d857d0cd85a7c57cea4d060bee4578d6cf5a14dfdf8cbf8784a", size = 22743, upload-time = "2026-05-28T03:32:52.175Z" },
+ { url = "https://files.pythonhosted.org/packages/81/e6/cd9575ac904136b3cbf7aa7ee819ef86eedb7274e46f230e94ea4342e729/platformdirs-4.10.0-py3-none-any.whl", hash = "sha256:fb516cdb12eb0d857d0cd85a7c57cea4d060bee4578d6cf5a14dfdf8cbf8784a", size = 22743, upload-time = "2026-05-28T03:32:52.175Z" },
]
[[package]]
@@ -854,7 +855,7 @@ version = "1.6.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
+ { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
]
[[package]]
@@ -862,14 +863,14 @@ name = "posthog"
version = "7.16.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
- { name = "backoff" },
- { name = "distro" },
- { name = "requests" },
- { name = "typing-extensions" },
+ { name = "backoff" },
+ { name = "distro" },
+ { name = "requests" },
+ { name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/b4/4f/a954175c862a3565d02c3f627874d85f18313472a0c4b08f45d84aaf3315/posthog-7.16.1.tar.gz", hash = "sha256:3619d3c619ad01f36c6d465e084950882417c63021eb3cfacacb23f900ec52d4", size = 226343, upload-time = "2026-05-27T18:46:20.129Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/3e/28/0f840699a1d0db3c1e5483c6208f0804a51f21ccfa34e6aa356161606adc/posthog-7.16.1-py3-none-any.whl", hash = "sha256:fd5aa4510033f3b039fda2fbfce45f493d140d4782f681e69639793dda317d67", size = 264231, upload-time = "2026-05-27T18:46:17.933Z" },
+ { url = "https://files.pythonhosted.org/packages/3e/28/0f840699a1d0db3c1e5483c6208f0804a51f21ccfa34e6aa356161606adc/posthog-7.16.1-py3-none-any.whl", hash = "sha256:fd5aa4510033f3b039fda2fbfce45f493d140d4782f681e69639793dda317d67", size = 264231, upload-time = "2026-05-27T18:46:17.933Z" },
]
[[package]]
@@ -877,15 +878,15 @@ name = "pre-commit"
version = "4.6.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
- { name = "cfgv" },
- { name = "identify" },
- { name = "nodeenv" },
- { name = "pyyaml" },
- { name = "virtualenv" },
+ { name = "cfgv" },
+ { name = "identify" },
+ { name = "nodeenv" },
+ { name = "pyyaml" },
+ { name = "virtualenv" },
]
sdist = { url = "https://files.pythonhosted.org/packages/8e/22/2de9408ac81acbb8a7d05d4cc064a152ccf33b3d480ebe0cd292153db239/pre_commit-4.6.0.tar.gz", hash = "sha256:718d2208cef53fdc38206e40524a6d4d9576d103eb16f0fec11c875e7716e9d9", size = 198525, upload-time = "2026-04-21T20:31:41.613Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/80/6e/4b28b62ecb6aae56769c34a8ff1d661473ec1e9519e2d5f8b2c150086b26/pre_commit-4.6.0-py2.py3-none-any.whl", hash = "sha256:e2cf246f7299edcabcf15f9b0571fdce06058527f0a06535068a86d38089f29b", size = 226472, upload-time = "2026-04-21T20:31:40.092Z" },
+ { url = "https://files.pythonhosted.org/packages/80/6e/4b28b62ecb6aae56769c34a8ff1d661473ec1e9519e2d5f8b2c150086b26/pre_commit-4.6.0-py2.py3-none-any.whl", hash = "sha256:e2cf246f7299edcabcf15f9b0571fdce06058527f0a06535068a86d38089f29b", size = 226472, upload-time = "2026-04-21T20:31:40.092Z" },
]
[[package]]
@@ -893,16 +894,16 @@ name = "psycopg"
version = "3.3.4"
source = { registry = "https://pypi.org/simple" }
dependencies = [
- { name = "tzdata", marker = "sys_platform == 'win32'" },
+ { name = "tzdata", marker = "sys_platform == 'win32'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/db/2f/cb91e5502ec9de1de6f1b76cfbf69531932725361168bb06963620c77e2e/psycopg-3.3.4.tar.gz", hash = "sha256:e21207764952cff81b6b8bdacad9a3939f2793367fdac2987b3aac36a651b5bc", size = 165799, upload-time = "2026-05-01T23:31:55.179Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/5c/e0/7b3dee031daae7743609ce3c746565d4a3ed7c2c186479eb48e34e838c64/psycopg-3.3.4-py3-none-any.whl", hash = "sha256:b6bbc25ccf05c8fad3b061d9db2ef0909a555171b84b07f29458a447253d679a", size = 213001, upload-time = "2026-05-01T23:20:50.816Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/e0/7b3dee031daae7743609ce3c746565d4a3ed7c2c186479eb48e34e838c64/psycopg-3.3.4-py3-none-any.whl", hash = "sha256:b6bbc25ccf05c8fad3b061d9db2ef0909a555171b84b07f29458a447253d679a", size = 213001, upload-time = "2026-05-01T23:20:50.816Z" },
]
[package.optional-dependencies]
binary = [
- { name = "psycopg-binary", marker = "implementation_name != 'pypy'" },
+ { name = "psycopg-binary", marker = "implementation_name != 'pypy'" },
]
[[package]]
@@ -910,28 +911,28 @@ name = "psycopg-binary"
version = "3.3.4"
source = { registry = "https://pypi.org/simple" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/09/43/13e9c406fbbf354580476e248a16b64802a376873ebe6339e30bb655572d/psycopg_binary-3.3.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:fbd1d4ed566895ad2d3bf4ddfd8bae90026930ddf29df3b9d91d32c8c47866a7", size = 4590377, upload-time = "2026-05-01T23:29:18.782Z" },
- { url = "https://files.pythonhosted.org/packages/22/be/2923cd7c3683e7afdecf4f10796a18de02f5c5ddc0969aa2ad0a8cdd3bbd/psycopg_binary-3.3.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:75a9067e236f9b9ae3535b66fe99bddb33d39c0de10112e49b9ab11eee53dc31", size = 4669023, upload-time = "2026-05-01T23:29:25.884Z" },
- { url = "https://files.pythonhosted.org/packages/96/a0/2c913d6fe13d6a8bd13597d36739bf47af063ad9399e402cfecab16f3c1e/psycopg_binary-3.3.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:b56b603ebcea8aa10b46228b8410ba7f13e7c2ee54389d4d9be0927fd8ce2a70", size = 5467423, upload-time = "2026-05-01T23:29:33.416Z" },
- { url = "https://files.pythonhosted.org/packages/e7/38/205d10bc1ad0df4a21c5c51659126bd3ea0ef98fcad1e852f78c249bb9c3/psycopg_binary-3.3.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c677c4ad433cb7150c8cd304a0769ae3bcfbe5ea0676eb53faa7b1443b16d0d3", size = 5151137, upload-time = "2026-05-01T23:29:42.013Z" },
- { url = "https://files.pythonhosted.org/packages/36/fc/f0381ddcd45eff3bb70dbca6823a996048d7f507b2ec3fc92c6fabc0fe87/psycopg_binary-3.3.4-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:26df2717e59c0473e4465a97dfb1b7afebaa479277870fd5784d1436470db47c", size = 6736671, upload-time = "2026-05-01T23:29:51.626Z" },
- { url = "https://files.pythonhosted.org/packages/95/40/fa545ae152c24327651e5624e4902121e808270be36c10b12e9939be09bc/psycopg_binary-3.3.4-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1dc1f79fd16bb1f3f4421417a514607539f17804d95c7ed617265369d1981cae", size = 4979601, upload-time = "2026-05-01T23:29:56.961Z" },
- { url = "https://files.pythonhosted.org/packages/86/e4/2f8a47ee97f90cd2b933d0463081d35631ff419de2b8c984a5f369857de0/psycopg_binary-3.3.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:136f199a407b5348b9b857c504aff60c77622a28482e7195839ce1b51238c4cc", size = 4510513, upload-time = "2026-05-01T23:30:07.243Z" },
- { url = "https://files.pythonhosted.org/packages/0e/0e/94e842ff4a7f98ed162580ca2e8b8864b28c1e0350f2443f8ee47f821167/psycopg_binary-3.3.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b6f5a29e9c775b9f12a1a717aa7a2c80f9e1db6f27ba44a5b59c80ac61d2ffcf", size = 4187243, upload-time = "2026-05-01T23:30:15.352Z" },
- { url = "https://files.pythonhosted.org/packages/d0/83/fc6c174b672e29b7de996ea77b6cbddf46c891751c3355f6974292baa6b4/psycopg_binary-3.3.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:ee17a2cf4943cde261adfad1bbc5bf38d6b3776d7afff74c7cabcbeaeb08c260", size = 3927347, upload-time = "2026-05-01T23:30:21.186Z" },
- { url = "https://files.pythonhosted.org/packages/e9/65/768364d4a97a15b1a7f47ba52688c1686f22941d8332a8398cefc468e25f/psycopg_binary-3.3.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5c4ab71be17bdca30cb34c34c4e1496e2f5d6f20c199c12bad226070b22ef9bf", size = 4236393, upload-time = "2026-05-01T23:30:26.211Z" },
- { url = "https://files.pythonhosted.org/packages/bd/3b/218efbc9e645becd80cdf651acda05f85cfe546b7a9c0458c7cbc8fe1f74/psycopg_binary-3.3.4-cp313-cp313-win_amd64.whl", hash = "sha256:dbfdb9b6cc79f31104a7b162a2b921b765fcc62af6c00540a167a8de47e4ed38", size = 3564592, upload-time = "2026-05-01T23:30:31.764Z" },
- { url = "https://files.pythonhosted.org/packages/48/a6/828c9185701dab71b234c2a76c38a08b098ebfec5020716b4e93807492b5/psycopg_binary-3.3.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:28b7398fdd19db3232c884fb24550bdfe951221f510e195e233299e4c9b78f97", size = 4607292, upload-time = "2026-05-01T23:30:38.962Z" },
- { url = "https://files.pythonhosted.org/packages/92/58/5b40dbc9d839045c9dae956960e4fb6d20bcabe6c59a2aa34fc3a371913f/psycopg_binary-3.3.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1fbaa292a3c8bb61b45df1ad3da1908ccee7cb889db9425e3557d9e34e2a4829", size = 4687023, upload-time = "2026-05-01T23:30:47.227Z" },
- { url = "https://files.pythonhosted.org/packages/85/a9/793f0ac107a9003b48441d0d1f9f616d96e0f37458dd8dc12528ceff55fb/psycopg_binary-3.3.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:94596f9e7633ee3f6440711d43bb70aa31cc0a46a900ab8b4201a366ace5c9e7", size = 5486985, upload-time = "2026-05-01T23:30:55.517Z" },
- { url = "https://files.pythonhosted.org/packages/8f/26/42e8533497e2592334f68ec529cf5f840f7fa4e99575a4bb61aa184dbfbf/psycopg_binary-3.3.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8c0056529e68dbe9184cd4019a1f3d8f3a4ead2f6fc7a5afcf27d3314edd1277", size = 5168745, upload-time = "2026-05-01T23:31:01.904Z" },
- { url = "https://files.pythonhosted.org/packages/15/af/b7151776cc08d5935d45c833ec818a9beb417cf7c08239af1aafbdae78ee/psycopg_binary-3.3.4-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2c09aad7051326e7603c14e50636db9c01f78272dc54b3accff03d46370461e6", size = 6761486, upload-time = "2026-05-01T23:31:14.511Z" },
- { url = "https://files.pythonhosted.org/packages/d0/ed/c92533b9124712d592cbf1cd6c76da933a2e0acea81dfe1fbe7e735f0cff/psycopg_binary-3.3.4-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:514404ed543efd620c85602b747df2a23cf1241b4067199e1a66f2d2757aaa41", size = 4997427, upload-time = "2026-05-01T23:31:20.901Z" },
- { url = "https://files.pythonhosted.org/packages/a2/23/ccadfd0de416aa188356daa199453af24087b042e296088706d190ae0295/psycopg_binary-3.3.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:46893c26858be12cc49ca4226ed6a60b4bfccadd946b3bebb783a60b38788228", size = 4533549, upload-time = "2026-05-01T23:31:26.204Z" },
- { url = "https://files.pythonhosted.org/packages/fd/a0/c8f43cee36386f7bc891ab41a9d31ea07cf9826038e732da79f26b1e5f34/psycopg_binary-3.3.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:df1d567fc430f6df15c9fcf67d87685fc49bdb325adc0db5af1adfb2f44eb5c9", size = 4210256, upload-time = "2026-05-01T23:31:33.884Z" },
- { url = "https://files.pythonhosted.org/packages/4e/2c/c1547871be3790676e8868b38655496422f94f0978dfb66b74bdba2f1676/psycopg_binary-3.3.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:6b9016b1714da4dd5ecaaa75b82098aa5a0b87854ce9b092e21c27c4ae23e014", size = 3946204, upload-time = "2026-05-01T23:31:39.626Z" },
- { url = "https://files.pythonhosted.org/packages/c4/b1/f6670f00fa7ea601584623f6c11602ab92117d83eaff885e0210f6de7418/psycopg_binary-3.3.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:47c656a8a7ba6eb0cff1801a4caaa9c8bdc12d03080e273aff1c8ac39971a77e", size = 4255811, upload-time = "2026-05-01T23:31:44.986Z" },
- { url = "https://files.pythonhosted.org/packages/eb/e6/5fff07a70d1f945ed90ae131c3bd76cab32beff7c58c6db15ad5820b6d1f/psycopg_binary-3.3.4-cp314-cp314-win_amd64.whl", hash = "sha256:c37e024c07308cd06cf3ec51bfd0e7f6157585a4d84d1bce4a7f5f7913719bf8", size = 3666849, upload-time = "2026-05-01T23:31:51.165Z" },
+ { url = "https://files.pythonhosted.org/packages/09/43/13e9c406fbbf354580476e248a16b64802a376873ebe6339e30bb655572d/psycopg_binary-3.3.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:fbd1d4ed566895ad2d3bf4ddfd8bae90026930ddf29df3b9d91d32c8c47866a7", size = 4590377, upload-time = "2026-05-01T23:29:18.782Z" },
+ { url = "https://files.pythonhosted.org/packages/22/be/2923cd7c3683e7afdecf4f10796a18de02f5c5ddc0969aa2ad0a8cdd3bbd/psycopg_binary-3.3.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:75a9067e236f9b9ae3535b66fe99bddb33d39c0de10112e49b9ab11eee53dc31", size = 4669023, upload-time = "2026-05-01T23:29:25.884Z" },
+ { url = "https://files.pythonhosted.org/packages/96/a0/2c913d6fe13d6a8bd13597d36739bf47af063ad9399e402cfecab16f3c1e/psycopg_binary-3.3.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:b56b603ebcea8aa10b46228b8410ba7f13e7c2ee54389d4d9be0927fd8ce2a70", size = 5467423, upload-time = "2026-05-01T23:29:33.416Z" },
+ { url = "https://files.pythonhosted.org/packages/e7/38/205d10bc1ad0df4a21c5c51659126bd3ea0ef98fcad1e852f78c249bb9c3/psycopg_binary-3.3.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c677c4ad433cb7150c8cd304a0769ae3bcfbe5ea0676eb53faa7b1443b16d0d3", size = 5151137, upload-time = "2026-05-01T23:29:42.013Z" },
+ { url = "https://files.pythonhosted.org/packages/36/fc/f0381ddcd45eff3bb70dbca6823a996048d7f507b2ec3fc92c6fabc0fe87/psycopg_binary-3.3.4-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:26df2717e59c0473e4465a97dfb1b7afebaa479277870fd5784d1436470db47c", size = 6736671, upload-time = "2026-05-01T23:29:51.626Z" },
+ { url = "https://files.pythonhosted.org/packages/95/40/fa545ae152c24327651e5624e4902121e808270be36c10b12e9939be09bc/psycopg_binary-3.3.4-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1dc1f79fd16bb1f3f4421417a514607539f17804d95c7ed617265369d1981cae", size = 4979601, upload-time = "2026-05-01T23:29:56.961Z" },
+ { url = "https://files.pythonhosted.org/packages/86/e4/2f8a47ee97f90cd2b933d0463081d35631ff419de2b8c984a5f369857de0/psycopg_binary-3.3.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:136f199a407b5348b9b857c504aff60c77622a28482e7195839ce1b51238c4cc", size = 4510513, upload-time = "2026-05-01T23:30:07.243Z" },
+ { url = "https://files.pythonhosted.org/packages/0e/0e/94e842ff4a7f98ed162580ca2e8b8864b28c1e0350f2443f8ee47f821167/psycopg_binary-3.3.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b6f5a29e9c775b9f12a1a717aa7a2c80f9e1db6f27ba44a5b59c80ac61d2ffcf", size = 4187243, upload-time = "2026-05-01T23:30:15.352Z" },
+ { url = "https://files.pythonhosted.org/packages/d0/83/fc6c174b672e29b7de996ea77b6cbddf46c891751c3355f6974292baa6b4/psycopg_binary-3.3.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:ee17a2cf4943cde261adfad1bbc5bf38d6b3776d7afff74c7cabcbeaeb08c260", size = 3927347, upload-time = "2026-05-01T23:30:21.186Z" },
+ { url = "https://files.pythonhosted.org/packages/e9/65/768364d4a97a15b1a7f47ba52688c1686f22941d8332a8398cefc468e25f/psycopg_binary-3.3.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5c4ab71be17bdca30cb34c34c4e1496e2f5d6f20c199c12bad226070b22ef9bf", size = 4236393, upload-time = "2026-05-01T23:30:26.211Z" },
+ { url = "https://files.pythonhosted.org/packages/bd/3b/218efbc9e645becd80cdf651acda05f85cfe546b7a9c0458c7cbc8fe1f74/psycopg_binary-3.3.4-cp313-cp313-win_amd64.whl", hash = "sha256:dbfdb9b6cc79f31104a7b162a2b921b765fcc62af6c00540a167a8de47e4ed38", size = 3564592, upload-time = "2026-05-01T23:30:31.764Z" },
+ { url = "https://files.pythonhosted.org/packages/48/a6/828c9185701dab71b234c2a76c38a08b098ebfec5020716b4e93807492b5/psycopg_binary-3.3.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:28b7398fdd19db3232c884fb24550bdfe951221f510e195e233299e4c9b78f97", size = 4607292, upload-time = "2026-05-01T23:30:38.962Z" },
+ { url = "https://files.pythonhosted.org/packages/92/58/5b40dbc9d839045c9dae956960e4fb6d20bcabe6c59a2aa34fc3a371913f/psycopg_binary-3.3.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1fbaa292a3c8bb61b45df1ad3da1908ccee7cb889db9425e3557d9e34e2a4829", size = 4687023, upload-time = "2026-05-01T23:30:47.227Z" },
+ { url = "https://files.pythonhosted.org/packages/85/a9/793f0ac107a9003b48441d0d1f9f616d96e0f37458dd8dc12528ceff55fb/psycopg_binary-3.3.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:94596f9e7633ee3f6440711d43bb70aa31cc0a46a900ab8b4201a366ace5c9e7", size = 5486985, upload-time = "2026-05-01T23:30:55.517Z" },
+ { url = "https://files.pythonhosted.org/packages/8f/26/42e8533497e2592334f68ec529cf5f840f7fa4e99575a4bb61aa184dbfbf/psycopg_binary-3.3.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8c0056529e68dbe9184cd4019a1f3d8f3a4ead2f6fc7a5afcf27d3314edd1277", size = 5168745, upload-time = "2026-05-01T23:31:01.904Z" },
+ { url = "https://files.pythonhosted.org/packages/15/af/b7151776cc08d5935d45c833ec818a9beb417cf7c08239af1aafbdae78ee/psycopg_binary-3.3.4-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2c09aad7051326e7603c14e50636db9c01f78272dc54b3accff03d46370461e6", size = 6761486, upload-time = "2026-05-01T23:31:14.511Z" },
+ { url = "https://files.pythonhosted.org/packages/d0/ed/c92533b9124712d592cbf1cd6c76da933a2e0acea81dfe1fbe7e735f0cff/psycopg_binary-3.3.4-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:514404ed543efd620c85602b747df2a23cf1241b4067199e1a66f2d2757aaa41", size = 4997427, upload-time = "2026-05-01T23:31:20.901Z" },
+ { url = "https://files.pythonhosted.org/packages/a2/23/ccadfd0de416aa188356daa199453af24087b042e296088706d190ae0295/psycopg_binary-3.3.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:46893c26858be12cc49ca4226ed6a60b4bfccadd946b3bebb783a60b38788228", size = 4533549, upload-time = "2026-05-01T23:31:26.204Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/a0/c8f43cee36386f7bc891ab41a9d31ea07cf9826038e732da79f26b1e5f34/psycopg_binary-3.3.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:df1d567fc430f6df15c9fcf67d87685fc49bdb325adc0db5af1adfb2f44eb5c9", size = 4210256, upload-time = "2026-05-01T23:31:33.884Z" },
+ { url = "https://files.pythonhosted.org/packages/4e/2c/c1547871be3790676e8868b38655496422f94f0978dfb66b74bdba2f1676/psycopg_binary-3.3.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:6b9016b1714da4dd5ecaaa75b82098aa5a0b87854ce9b092e21c27c4ae23e014", size = 3946204, upload-time = "2026-05-01T23:31:39.626Z" },
+ { url = "https://files.pythonhosted.org/packages/c4/b1/f6670f00fa7ea601584623f6c11602ab92117d83eaff885e0210f6de7418/psycopg_binary-3.3.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:47c656a8a7ba6eb0cff1801a4caaa9c8bdc12d03080e273aff1c8ac39971a77e", size = 4255811, upload-time = "2026-05-01T23:31:44.986Z" },
+ { url = "https://files.pythonhosted.org/packages/eb/e6/5fff07a70d1f945ed90ae131c3bd76cab32beff7c58c6db15ad5820b6d1f/psycopg_binary-3.3.4-cp314-cp314-win_amd64.whl", hash = "sha256:c37e024c07308cd06cf3ec51bfd0e7f6157585a4d84d1bce4a7f5f7913719bf8", size = 3666849, upload-time = "2026-05-01T23:31:51.165Z" },
]
[[package]]
@@ -939,14 +940,14 @@ name = "pydantic"
version = "2.13.4"
source = { registry = "https://pypi.org/simple" }
dependencies = [
- { name = "annotated-types" },
- { name = "pydantic-core" },
- { name = "typing-extensions" },
- { name = "typing-inspection" },
+ { name = "annotated-types" },
+ { name = "pydantic-core" },
+ { name = "typing-extensions" },
+ { name = "typing-inspection" },
]
sdist = { url = "https://files.pythonhosted.org/packages/18/a5/b60d21ac674192f8ab0ba4e9fd860690f9b4a6e51ca5df118733b487d8d6/pydantic-2.13.4.tar.gz", hash = "sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6", size = 844775, upload-time = "2026-05-06T13:43:05.343Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl", hash = "sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba", size = 472262, upload-time = "2026-05-06T13:43:02.641Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl", hash = "sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba", size = 472262, upload-time = "2026-05-06T13:43:02.641Z" },
]
[[package]]
@@ -954,55 +955,55 @@ name = "pydantic-core"
version = "2.46.4"
source = { registry = "https://pypi.org/simple" }
dependencies = [
- { name = "typing-extensions" },
+ { name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/9d/56/921726b776ace8d8f5db44c4ef961006580d91dc52b803c489fafd1aa249/pydantic_core-2.46.4.tar.gz", hash = "sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1", size = 471464, upload-time = "2026-05-06T13:37:06.98Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/51/a2/5d30b469c5267a17b39dec53208222f76a8d351dfac4af661888c5aee77d/pydantic_core-2.46.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:5d5902252db0d3cedf8d4a1bc68f70eeb430f7e4c7104c8c476753519b423008", size = 2106306, upload-time = "2026-05-06T13:37:48.029Z" },
- { url = "https://files.pythonhosted.org/packages/c1/81/4fa520eaffa8bd7d1525e644cd6d39e7d60b1592bc5b516693c7340b50f1/pydantic_core-2.46.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c94f0688e7b8d0a67abf40e57a7eaaecd17cc9586706a31b76c031f63df052b4", size = 1951906, upload-time = "2026-05-06T13:37:17.012Z" },
- { url = "https://files.pythonhosted.org/packages/03/d5/fd02da45b659668b05923b17ba3a0100a0a3d5541e3bd8fcc4ecb711309e/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f027324c56cd5406ca49c124b0db10e56c69064fec039acc571c29020cc87c76", size = 1976802, upload-time = "2026-05-06T13:37:35.113Z" },
- { url = "https://files.pythonhosted.org/packages/21/f2/95727e1368be3d3ed485eaab7adbd7dda408f33f7a36e8b48e0144002b91/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e739fee756ba1010f8bcccb534252e85a35fe45ae92c295a06059ce58b74ccd3", size = 2052446, upload-time = "2026-05-06T13:37:12.313Z" },
- { url = "https://files.pythonhosted.org/packages/9c/86/5d99feea3f77c7234b8718075b23db11532773c1a0dbd9b9490215dc2eeb/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d56801be94b86a9da183e5f3766e6310752b99ff647e38b09a9500d88e46e76", size = 2232757, upload-time = "2026-05-06T13:39:01.149Z" },
- { url = "https://files.pythonhosted.org/packages/d2/3a/508ac615935ef7588cf6d9e9b91309fdc2da751af865e02a9098de88258c/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2412e734dcb48da14d4e4006b82b46b74f2518b8a26ee7e58c6844a6cd6d03c4", size = 2309275, upload-time = "2026-05-06T13:37:41.406Z" },
- { url = "https://files.pythonhosted.org/packages/07/f8/41db9de19d7987d6b04715a02b3b40aea467000275d9d758ffaa31af7d50/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9551187363ffc0de2a00b2e47c25aeaeb1020b69b668762966df15fc5659dd5a", size = 2094467, upload-time = "2026-05-06T13:39:18.847Z" },
- { url = "https://files.pythonhosted.org/packages/2c/e2/f35033184cb11d0052daf4416e8e10a502ea2ac006fc4f459aee872727d1/pydantic_core-2.46.4-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:0186750b482eefa11d7f435892b09c5c606193ef3375bcf94aa00ae6bfb66262", size = 2134417, upload-time = "2026-05-06T13:40:17.944Z" },
- { url = "https://files.pythonhosted.org/packages/7e/7b/6ceeb1cc90e193862f444ebe373d8fdf613f0a82572dde03fb10734c6c71/pydantic_core-2.46.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5855698a4856556d86e8e6cd8434bc3ac0314ee8e12089ae0e143f64c6256e4e", size = 2179782, upload-time = "2026-05-06T13:40:32.618Z" },
- { url = "https://files.pythonhosted.org/packages/5a/f2/c8d7773ede6af08036423a00ae0ceffce266c3c52a096c435d68c896083f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:cbaf13819775b7f769bf4a1f066cb6df7a28d4480081a589828ef190226881cd", size = 2188782, upload-time = "2026-05-06T13:36:51.018Z" },
- { url = "https://files.pythonhosted.org/packages/59/31/0c864784e31f09f05cdd87606f08923b9c9e7f6e51dd27f20f62f975ce9f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:633147d34cf4550417f12e2b1a0383973bdf5cdfde212cb09e9a581cf10820be", size = 2328334, upload-time = "2026-05-06T13:40:37.764Z" },
- { url = "https://files.pythonhosted.org/packages/c2/eb/4f6c8a41efa30baa755590f4141abf3a8c370fab610915733e74134a7270/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:82cf5301172168103724d49a1444d3378cb20cdee30b116a1bd6031236298a5d", size = 2372986, upload-time = "2026-05-06T13:39:34.152Z" },
- { url = "https://files.pythonhosted.org/packages/5b/24/b375a480d53113860c299764bfe9f349a3dc9108b3adc0d7f0d786492ebf/pydantic_core-2.46.4-cp313-cp313-win32.whl", hash = "sha256:9fa8ae11da9e2b3126c6426f147e0fba88d96d65921799bb30c6abd1cb2c97fb", size = 1973693, upload-time = "2026-05-06T13:37:55.072Z" },
- { url = "https://files.pythonhosted.org/packages/7e/e8/cff247591966f2d22ec8c003cd7587e27b7ba7b81ab2fb888e3ab75dc285/pydantic_core-2.46.4-cp313-cp313-win_amd64.whl", hash = "sha256:6b3ace8194b0e5204818c92802dcdca7fc6d88aabbb799d7c795540d9cd6d292", size = 2071819, upload-time = "2026-05-06T13:38:49.139Z" },
- { url = "https://files.pythonhosted.org/packages/c6/1a/f4aee670d5670e9e148e0c82c7db98d780be566c6e6a97ee8035528ca0b3/pydantic_core-2.46.4-cp313-cp313-win_arm64.whl", hash = "sha256:184c081504d17f1c1066e430e117142b2c77d9448a97f7b65c6ac9fd9aee238d", size = 2027411, upload-time = "2026-05-06T13:40:45.796Z" },
- { url = "https://files.pythonhosted.org/packages/8d/74/228a26ddad29c6672b805d9fd78e8d251cd04004fa7eed0e622096cd0250/pydantic_core-2.46.4-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:428e04521a40150c85216fc8b85e8d39fece235a9cf5e383761238c7fa9b96fb", size = 2102079, upload-time = "2026-05-06T13:38:41.019Z" },
- { url = "https://files.pythonhosted.org/packages/ad/1f/8970b150a4b4365623ae00fc88603491f763c627311ae8031e3111356d6e/pydantic_core-2.46.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23ace664830ee0bfe014a0c7bc248b1f7f25ed7ad103852c317624a1083af462", size = 1952179, upload-time = "2026-05-06T13:36:59.812Z" },
- { url = "https://files.pythonhosted.org/packages/95/30/5211a831ae054928054b2f79731661087a2bc5c01e825c672b3a4a8f1b3e/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce5c1d2a8b27468f433ca974829c44060b8097eedc39933e3c206a90ee49c4a9", size = 1978926, upload-time = "2026-05-06T13:37:39.933Z" },
- { url = "https://files.pythonhosted.org/packages/57/e9/689668733b1eb67adeef047db3c2e8788fcf65a7fd9c9e2b46b7744fe245/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7283d57845ecf5a163403eb0702dfc220cc4fbdd18919cb5ccea4f95ee1cdab4", size = 2046785, upload-time = "2026-05-06T13:38:01.995Z" },
- { url = "https://files.pythonhosted.org/packages/60/d9/6715260422ff50a2109878fd24d948a6c3446bb2664f34ee78cd972b3acd/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8daafc69c93ee8a0204506a3b6b30f586ef54028f52aeeeb5c4cfc5184fd5914", size = 2228733, upload-time = "2026-05-06T13:40:50.371Z" },
- { url = "https://files.pythonhosted.org/packages/18/ae/fdb2f64316afca925640f8e70bb1a564b0ec2721c1389e25b8eb4bf9a299/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd2213145bcc2ba85884d0ac63d222fece9209678f77b9b4d76f054c561adb28", size = 2307534, upload-time = "2026-05-06T13:37:21.531Z" },
- { url = "https://files.pythonhosted.org/packages/89/1d/8eff589b45bb8190a9d12c49cfad0f176a5cbd1534908a6b5125e2886239/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a5f930472650a82629163023e630d160863fce524c616f4e5186e5de9d9a49b", size = 2099732, upload-time = "2026-05-06T13:39:31.942Z" },
- { url = "https://files.pythonhosted.org/packages/06/d5/ee5a3366637fee41dee51a1fc91562dcf12ddbc68fda34e6b253da2324bb/pydantic_core-2.46.4-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:c1b3f518abeca3aa13c712fd202306e145abf59a18b094a6bafb2d2bbf59192c", size = 2129627, upload-time = "2026-05-06T13:37:25.033Z" },
- { url = "https://files.pythonhosted.org/packages/94/33/2414be571d2c6a6c4d08be21f9292b6d3fdb08949a97b6dfe985017821db/pydantic_core-2.46.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a7dd0b3ee80d90150e3495a3a13ac34dbcbfd4f012996a6a1d8900e91b5c0fb", size = 2179141, upload-time = "2026-05-06T13:37:14.046Z" },
- { url = "https://files.pythonhosted.org/packages/7b/79/7daa95be995be0eecc4cf75064cb33f9bbbfe3fe0158caf2f0d4a996a5c7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:3fb702cd90b0446a3a1c5e470bfa0dd23c0233b676a9099ddcc964fa6ca13898", size = 2184325, upload-time = "2026-05-06T13:36:53.615Z" },
- { url = "https://files.pythonhosted.org/packages/9f/cb/d0a382f5c0de8a222dc61c65348e0ce831b1f68e0a018450d31c2cace3a5/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b8458003118a712e66286df6a707db01c52c0f52f7db8e4a38f0da1d3b94fc4e", size = 2323990, upload-time = "2026-05-06T13:40:29.971Z" },
- { url = "https://files.pythonhosted.org/packages/05/db/d9ba624cc4a5aced1598e88c04fdbd8310c8a69b9d38b9a3d39ce3a61ed7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:372429a130e469c9cd698925ce5fc50940b7a1336b0d82038e63d5bbc4edc519", size = 2369978, upload-time = "2026-05-06T13:37:23.027Z" },
- { url = "https://files.pythonhosted.org/packages/f2/20/d15df15ba918c423461905802bfd2981c3af0bfa0e40d05e13edbfa48bc3/pydantic_core-2.46.4-cp314-cp314-win32.whl", hash = "sha256:85bb3611ff1802f3ee7fdd7dbff26b56f343fb432d57a4728fdd49b6ef35e2f4", size = 1966354, upload-time = "2026-05-06T13:38:03.499Z" },
- { url = "https://files.pythonhosted.org/packages/fc/b6/6b8de4c0a7d7ab3004c439c80c5c1e0a3e8d78bbae19379b01960383d9e5/pydantic_core-2.46.4-cp314-cp314-win_amd64.whl", hash = "sha256:811ff8e9c313ab425368bcbb36e5c4ebd7108c2bbf4e4089cfbb0b01eff63fac", size = 2072238, upload-time = "2026-05-06T13:39:40.807Z" },
- { url = "https://files.pythonhosted.org/packages/32/36/51eb763beec1f4cf59b1db243a7dcc39cbb41230f050a09b9d69faaf0a48/pydantic_core-2.46.4-cp314-cp314-win_arm64.whl", hash = "sha256:bfec22eab3c8cc2ceec0248aec886624116dc079afa027ecc8ad4a7e62010f8a", size = 2018251, upload-time = "2026-05-06T13:37:26.72Z" },
- { url = "https://files.pythonhosted.org/packages/e8/91/855af51d625b23aa987116a19e231d2aaef9c4a415273ddc189b79a45fee/pydantic_core-2.46.4-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:af8244b2bef6aaad6d92cda81372de7f8c8d36c9f0c3ea36e827c60e7d9467a0", size = 2099593, upload-time = "2026-05-06T13:39:47.682Z" },
- { url = "https://files.pythonhosted.org/packages/fb/1b/8784a54c65edb5f49f0a14d6977cf1b209bba85a4c77445b255c2de58ab3/pydantic_core-2.46.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a4330cdbc57162e4b3aa303f588ba752257694c9c9be3e7ebb11b4aca659b5d", size = 1935226, upload-time = "2026-05-06T13:40:40.428Z" },
- { url = "https://files.pythonhosted.org/packages/e8/e7/1955d28d1afc56dd4b3ad7cc0cf39df1b9852964cf16e5d13912756d6d6b/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29c61fc04a3d840155ff08e475a04809278972fe6aef51e2720554e96367e34b", size = 1974605, upload-time = "2026-05-06T13:37:32.029Z" },
- { url = "https://files.pythonhosted.org/packages/93/e2/3fedbf0ba7a22850e6e9fd78117f1c0f10f950182344d8a6c535d468fdd8/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c50f2528cf200c5eed56faf3f4e22fcd5f38c157a8b78576e6ba3168ec35f000", size = 2030777, upload-time = "2026-05-06T13:38:55.239Z" },
- { url = "https://files.pythonhosted.org/packages/f8/61/46be275fcaaba0b4f5b9669dd852267ce1ff616592dccf7a7845588df091/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0cbe8b01f948de4286c74cdd6c667aceb38f5c1e26f0693b3983d9d74887c65e", size = 2236641, upload-time = "2026-05-06T13:37:08.096Z" },
- { url = "https://files.pythonhosted.org/packages/60/db/12e93e46a8bac9988be3c016860f83293daea8c716c029c9ace279036f2f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:617d7e2ca7dcb8c5cf6bcb8c59b8832c94b36196bbf1cbd1bfb56ed341905edd", size = 2286404, upload-time = "2026-05-06T13:40:20.221Z" },
- { url = "https://files.pythonhosted.org/packages/e2/4a/4d8b19008f38d31c53b8219cfedc2e3d5de5fe99d90076b7e767de29274f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7027560ee92211647d0d34e3f7cd6f50da56399d26a9c8ad0da286d3869a53f3", size = 2109219, upload-time = "2026-05-06T13:38:12.153Z" },
- { url = "https://files.pythonhosted.org/packages/88/70/3cbc40978fefb7bb09c6708d40d4ad1a5d70fd7213c3d17f971de868ec1f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:f99626688942fb746e545232e7726926f3be91b5975f8b55327665fafda991c7", size = 2110594, upload-time = "2026-05-06T13:40:02.971Z" },
- { url = "https://files.pythonhosted.org/packages/9d/20/b8d36736216e29491125531685b2f9e61aa5b4b2599893f8268551da3338/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fc3e9034a63de20e15e8ade85358bc6efc614008cab72898b4b4952bea0509ff", size = 2159542, upload-time = "2026-05-06T13:39:27.506Z" },
- { url = "https://files.pythonhosted.org/packages/1d/a2/367df868eb584dacf6bf82a389272406d7178e301c4ac82545ab98bc2dd9/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:97e7cf2be5c77b7d1a9713a05605d49460d02c6078d38d8bef3cbe323c548424", size = 2168146, upload-time = "2026-05-06T13:38:31.93Z" },
- { url = "https://files.pythonhosted.org/packages/c1/b8/4460f77f7e201893f649a29ab355dddd3beee8a97bcb1a320db414f9a06e/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:3bf92c5d0e00fefaab325a4d27828fe6b6e2a21848686b5b60d2d9eeb09d76c6", size = 2306309, upload-time = "2026-05-06T13:37:44.717Z" },
- { url = "https://files.pythonhosted.org/packages/64/c4/be2639293acd87dc8ddbcec41a73cee9b2ebf996fe6d892a1a74e88ad3f7/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:3ecbc122d18468d06ca279dc26a8c2e2d5acb10943bb35e36ae92096dc3b5565", size = 2369736, upload-time = "2026-05-06T13:37:05.645Z" },
- { url = "https://files.pythonhosted.org/packages/30/a6/9f9f380dbb301f67023bf8f707aaa75daadf84f7152d95c410fd7e81d994/pydantic_core-2.46.4-cp314-cp314t-win32.whl", hash = "sha256:e846ae7835bf0703ae43f534ab79a867146dadd59dc9ca5c8b53d5c8f7c9ef02", size = 1955575, upload-time = "2026-05-06T13:38:51.116Z" },
- { url = "https://files.pythonhosted.org/packages/40/1f/f1eb9eb350e795d1af8586289746f5c5677d16043040d63710e22abc43c9/pydantic_core-2.46.4-cp314-cp314t-win_amd64.whl", hash = "sha256:2108ba5c1c1eca18030634489dc544844144ee36357f2f9f780b93e7ddbb44b5", size = 2051624, upload-time = "2026-05-06T13:38:21.672Z" },
- { url = "https://files.pythonhosted.org/packages/f6/d2/42dd53d0a85c27606f316d3aa5d2869c4e8470a5ed6dec30e4a1abe19192/pydantic_core-2.46.4-cp314-cp314t-win_arm64.whl", hash = "sha256:4fcbe087dbc2068af7eda3aa87634eba216dbda64d1ae73c8684b621d33f6596", size = 2017325, upload-time = "2026-05-06T13:40:52.723Z" },
+ { url = "https://files.pythonhosted.org/packages/51/a2/5d30b469c5267a17b39dec53208222f76a8d351dfac4af661888c5aee77d/pydantic_core-2.46.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:5d5902252db0d3cedf8d4a1bc68f70eeb430f7e4c7104c8c476753519b423008", size = 2106306, upload-time = "2026-05-06T13:37:48.029Z" },
+ { url = "https://files.pythonhosted.org/packages/c1/81/4fa520eaffa8bd7d1525e644cd6d39e7d60b1592bc5b516693c7340b50f1/pydantic_core-2.46.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c94f0688e7b8d0a67abf40e57a7eaaecd17cc9586706a31b76c031f63df052b4", size = 1951906, upload-time = "2026-05-06T13:37:17.012Z" },
+ { url = "https://files.pythonhosted.org/packages/03/d5/fd02da45b659668b05923b17ba3a0100a0a3d5541e3bd8fcc4ecb711309e/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f027324c56cd5406ca49c124b0db10e56c69064fec039acc571c29020cc87c76", size = 1976802, upload-time = "2026-05-06T13:37:35.113Z" },
+ { url = "https://files.pythonhosted.org/packages/21/f2/95727e1368be3d3ed485eaab7adbd7dda408f33f7a36e8b48e0144002b91/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e739fee756ba1010f8bcccb534252e85a35fe45ae92c295a06059ce58b74ccd3", size = 2052446, upload-time = "2026-05-06T13:37:12.313Z" },
+ { url = "https://files.pythonhosted.org/packages/9c/86/5d99feea3f77c7234b8718075b23db11532773c1a0dbd9b9490215dc2eeb/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d56801be94b86a9da183e5f3766e6310752b99ff647e38b09a9500d88e46e76", size = 2232757, upload-time = "2026-05-06T13:39:01.149Z" },
+ { url = "https://files.pythonhosted.org/packages/d2/3a/508ac615935ef7588cf6d9e9b91309fdc2da751af865e02a9098de88258c/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2412e734dcb48da14d4e4006b82b46b74f2518b8a26ee7e58c6844a6cd6d03c4", size = 2309275, upload-time = "2026-05-06T13:37:41.406Z" },
+ { url = "https://files.pythonhosted.org/packages/07/f8/41db9de19d7987d6b04715a02b3b40aea467000275d9d758ffaa31af7d50/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9551187363ffc0de2a00b2e47c25aeaeb1020b69b668762966df15fc5659dd5a", size = 2094467, upload-time = "2026-05-06T13:39:18.847Z" },
+ { url = "https://files.pythonhosted.org/packages/2c/e2/f35033184cb11d0052daf4416e8e10a502ea2ac006fc4f459aee872727d1/pydantic_core-2.46.4-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:0186750b482eefa11d7f435892b09c5c606193ef3375bcf94aa00ae6bfb66262", size = 2134417, upload-time = "2026-05-06T13:40:17.944Z" },
+ { url = "https://files.pythonhosted.org/packages/7e/7b/6ceeb1cc90e193862f444ebe373d8fdf613f0a82572dde03fb10734c6c71/pydantic_core-2.46.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5855698a4856556d86e8e6cd8434bc3ac0314ee8e12089ae0e143f64c6256e4e", size = 2179782, upload-time = "2026-05-06T13:40:32.618Z" },
+ { url = "https://files.pythonhosted.org/packages/5a/f2/c8d7773ede6af08036423a00ae0ceffce266c3c52a096c435d68c896083f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:cbaf13819775b7f769bf4a1f066cb6df7a28d4480081a589828ef190226881cd", size = 2188782, upload-time = "2026-05-06T13:36:51.018Z" },
+ { url = "https://files.pythonhosted.org/packages/59/31/0c864784e31f09f05cdd87606f08923b9c9e7f6e51dd27f20f62f975ce9f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:633147d34cf4550417f12e2b1a0383973bdf5cdfde212cb09e9a581cf10820be", size = 2328334, upload-time = "2026-05-06T13:40:37.764Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/eb/4f6c8a41efa30baa755590f4141abf3a8c370fab610915733e74134a7270/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:82cf5301172168103724d49a1444d3378cb20cdee30b116a1bd6031236298a5d", size = 2372986, upload-time = "2026-05-06T13:39:34.152Z" },
+ { url = "https://files.pythonhosted.org/packages/5b/24/b375a480d53113860c299764bfe9f349a3dc9108b3adc0d7f0d786492ebf/pydantic_core-2.46.4-cp313-cp313-win32.whl", hash = "sha256:9fa8ae11da9e2b3126c6426f147e0fba88d96d65921799bb30c6abd1cb2c97fb", size = 1973693, upload-time = "2026-05-06T13:37:55.072Z" },
+ { url = "https://files.pythonhosted.org/packages/7e/e8/cff247591966f2d22ec8c003cd7587e27b7ba7b81ab2fb888e3ab75dc285/pydantic_core-2.46.4-cp313-cp313-win_amd64.whl", hash = "sha256:6b3ace8194b0e5204818c92802dcdca7fc6d88aabbb799d7c795540d9cd6d292", size = 2071819, upload-time = "2026-05-06T13:38:49.139Z" },
+ { url = "https://files.pythonhosted.org/packages/c6/1a/f4aee670d5670e9e148e0c82c7db98d780be566c6e6a97ee8035528ca0b3/pydantic_core-2.46.4-cp313-cp313-win_arm64.whl", hash = "sha256:184c081504d17f1c1066e430e117142b2c77d9448a97f7b65c6ac9fd9aee238d", size = 2027411, upload-time = "2026-05-06T13:40:45.796Z" },
+ { url = "https://files.pythonhosted.org/packages/8d/74/228a26ddad29c6672b805d9fd78e8d251cd04004fa7eed0e622096cd0250/pydantic_core-2.46.4-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:428e04521a40150c85216fc8b85e8d39fece235a9cf5e383761238c7fa9b96fb", size = 2102079, upload-time = "2026-05-06T13:38:41.019Z" },
+ { url = "https://files.pythonhosted.org/packages/ad/1f/8970b150a4b4365623ae00fc88603491f763c627311ae8031e3111356d6e/pydantic_core-2.46.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23ace664830ee0bfe014a0c7bc248b1f7f25ed7ad103852c317624a1083af462", size = 1952179, upload-time = "2026-05-06T13:36:59.812Z" },
+ { url = "https://files.pythonhosted.org/packages/95/30/5211a831ae054928054b2f79731661087a2bc5c01e825c672b3a4a8f1b3e/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce5c1d2a8b27468f433ca974829c44060b8097eedc39933e3c206a90ee49c4a9", size = 1978926, upload-time = "2026-05-06T13:37:39.933Z" },
+ { url = "https://files.pythonhosted.org/packages/57/e9/689668733b1eb67adeef047db3c2e8788fcf65a7fd9c9e2b46b7744fe245/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7283d57845ecf5a163403eb0702dfc220cc4fbdd18919cb5ccea4f95ee1cdab4", size = 2046785, upload-time = "2026-05-06T13:38:01.995Z" },
+ { url = "https://files.pythonhosted.org/packages/60/d9/6715260422ff50a2109878fd24d948a6c3446bb2664f34ee78cd972b3acd/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8daafc69c93ee8a0204506a3b6b30f586ef54028f52aeeeb5c4cfc5184fd5914", size = 2228733, upload-time = "2026-05-06T13:40:50.371Z" },
+ { url = "https://files.pythonhosted.org/packages/18/ae/fdb2f64316afca925640f8e70bb1a564b0ec2721c1389e25b8eb4bf9a299/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd2213145bcc2ba85884d0ac63d222fece9209678f77b9b4d76f054c561adb28", size = 2307534, upload-time = "2026-05-06T13:37:21.531Z" },
+ { url = "https://files.pythonhosted.org/packages/89/1d/8eff589b45bb8190a9d12c49cfad0f176a5cbd1534908a6b5125e2886239/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a5f930472650a82629163023e630d160863fce524c616f4e5186e5de9d9a49b", size = 2099732, upload-time = "2026-05-06T13:39:31.942Z" },
+ { url = "https://files.pythonhosted.org/packages/06/d5/ee5a3366637fee41dee51a1fc91562dcf12ddbc68fda34e6b253da2324bb/pydantic_core-2.46.4-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:c1b3f518abeca3aa13c712fd202306e145abf59a18b094a6bafb2d2bbf59192c", size = 2129627, upload-time = "2026-05-06T13:37:25.033Z" },
+ { url = "https://files.pythonhosted.org/packages/94/33/2414be571d2c6a6c4d08be21f9292b6d3fdb08949a97b6dfe985017821db/pydantic_core-2.46.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a7dd0b3ee80d90150e3495a3a13ac34dbcbfd4f012996a6a1d8900e91b5c0fb", size = 2179141, upload-time = "2026-05-06T13:37:14.046Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/79/7daa95be995be0eecc4cf75064cb33f9bbbfe3fe0158caf2f0d4a996a5c7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:3fb702cd90b0446a3a1c5e470bfa0dd23c0233b676a9099ddcc964fa6ca13898", size = 2184325, upload-time = "2026-05-06T13:36:53.615Z" },
+ { url = "https://files.pythonhosted.org/packages/9f/cb/d0a382f5c0de8a222dc61c65348e0ce831b1f68e0a018450d31c2cace3a5/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b8458003118a712e66286df6a707db01c52c0f52f7db8e4a38f0da1d3b94fc4e", size = 2323990, upload-time = "2026-05-06T13:40:29.971Z" },
+ { url = "https://files.pythonhosted.org/packages/05/db/d9ba624cc4a5aced1598e88c04fdbd8310c8a69b9d38b9a3d39ce3a61ed7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:372429a130e469c9cd698925ce5fc50940b7a1336b0d82038e63d5bbc4edc519", size = 2369978, upload-time = "2026-05-06T13:37:23.027Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/20/d15df15ba918c423461905802bfd2981c3af0bfa0e40d05e13edbfa48bc3/pydantic_core-2.46.4-cp314-cp314-win32.whl", hash = "sha256:85bb3611ff1802f3ee7fdd7dbff26b56f343fb432d57a4728fdd49b6ef35e2f4", size = 1966354, upload-time = "2026-05-06T13:38:03.499Z" },
+ { url = "https://files.pythonhosted.org/packages/fc/b6/6b8de4c0a7d7ab3004c439c80c5c1e0a3e8d78bbae19379b01960383d9e5/pydantic_core-2.46.4-cp314-cp314-win_amd64.whl", hash = "sha256:811ff8e9c313ab425368bcbb36e5c4ebd7108c2bbf4e4089cfbb0b01eff63fac", size = 2072238, upload-time = "2026-05-06T13:39:40.807Z" },
+ { url = "https://files.pythonhosted.org/packages/32/36/51eb763beec1f4cf59b1db243a7dcc39cbb41230f050a09b9d69faaf0a48/pydantic_core-2.46.4-cp314-cp314-win_arm64.whl", hash = "sha256:bfec22eab3c8cc2ceec0248aec886624116dc079afa027ecc8ad4a7e62010f8a", size = 2018251, upload-time = "2026-05-06T13:37:26.72Z" },
+ { url = "https://files.pythonhosted.org/packages/e8/91/855af51d625b23aa987116a19e231d2aaef9c4a415273ddc189b79a45fee/pydantic_core-2.46.4-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:af8244b2bef6aaad6d92cda81372de7f8c8d36c9f0c3ea36e827c60e7d9467a0", size = 2099593, upload-time = "2026-05-06T13:39:47.682Z" },
+ { url = "https://files.pythonhosted.org/packages/fb/1b/8784a54c65edb5f49f0a14d6977cf1b209bba85a4c77445b255c2de58ab3/pydantic_core-2.46.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a4330cdbc57162e4b3aa303f588ba752257694c9c9be3e7ebb11b4aca659b5d", size = 1935226, upload-time = "2026-05-06T13:40:40.428Z" },
+ { url = "https://files.pythonhosted.org/packages/e8/e7/1955d28d1afc56dd4b3ad7cc0cf39df1b9852964cf16e5d13912756d6d6b/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29c61fc04a3d840155ff08e475a04809278972fe6aef51e2720554e96367e34b", size = 1974605, upload-time = "2026-05-06T13:37:32.029Z" },
+ { url = "https://files.pythonhosted.org/packages/93/e2/3fedbf0ba7a22850e6e9fd78117f1c0f10f950182344d8a6c535d468fdd8/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c50f2528cf200c5eed56faf3f4e22fcd5f38c157a8b78576e6ba3168ec35f000", size = 2030777, upload-time = "2026-05-06T13:38:55.239Z" },
+ { url = "https://files.pythonhosted.org/packages/f8/61/46be275fcaaba0b4f5b9669dd852267ce1ff616592dccf7a7845588df091/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0cbe8b01f948de4286c74cdd6c667aceb38f5c1e26f0693b3983d9d74887c65e", size = 2236641, upload-time = "2026-05-06T13:37:08.096Z" },
+ { url = "https://files.pythonhosted.org/packages/60/db/12e93e46a8bac9988be3c016860f83293daea8c716c029c9ace279036f2f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:617d7e2ca7dcb8c5cf6bcb8c59b8832c94b36196bbf1cbd1bfb56ed341905edd", size = 2286404, upload-time = "2026-05-06T13:40:20.221Z" },
+ { url = "https://files.pythonhosted.org/packages/e2/4a/4d8b19008f38d31c53b8219cfedc2e3d5de5fe99d90076b7e767de29274f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7027560ee92211647d0d34e3f7cd6f50da56399d26a9c8ad0da286d3869a53f3", size = 2109219, upload-time = "2026-05-06T13:38:12.153Z" },
+ { url = "https://files.pythonhosted.org/packages/88/70/3cbc40978fefb7bb09c6708d40d4ad1a5d70fd7213c3d17f971de868ec1f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:f99626688942fb746e545232e7726926f3be91b5975f8b55327665fafda991c7", size = 2110594, upload-time = "2026-05-06T13:40:02.971Z" },
+ { url = "https://files.pythonhosted.org/packages/9d/20/b8d36736216e29491125531685b2f9e61aa5b4b2599893f8268551da3338/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fc3e9034a63de20e15e8ade85358bc6efc614008cab72898b4b4952bea0509ff", size = 2159542, upload-time = "2026-05-06T13:39:27.506Z" },
+ { url = "https://files.pythonhosted.org/packages/1d/a2/367df868eb584dacf6bf82a389272406d7178e301c4ac82545ab98bc2dd9/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:97e7cf2be5c77b7d1a9713a05605d49460d02c6078d38d8bef3cbe323c548424", size = 2168146, upload-time = "2026-05-06T13:38:31.93Z" },
+ { url = "https://files.pythonhosted.org/packages/c1/b8/4460f77f7e201893f649a29ab355dddd3beee8a97bcb1a320db414f9a06e/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:3bf92c5d0e00fefaab325a4d27828fe6b6e2a21848686b5b60d2d9eeb09d76c6", size = 2306309, upload-time = "2026-05-06T13:37:44.717Z" },
+ { url = "https://files.pythonhosted.org/packages/64/c4/be2639293acd87dc8ddbcec41a73cee9b2ebf996fe6d892a1a74e88ad3f7/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:3ecbc122d18468d06ca279dc26a8c2e2d5acb10943bb35e36ae92096dc3b5565", size = 2369736, upload-time = "2026-05-06T13:37:05.645Z" },
+ { url = "https://files.pythonhosted.org/packages/30/a6/9f9f380dbb301f67023bf8f707aaa75daadf84f7152d95c410fd7e81d994/pydantic_core-2.46.4-cp314-cp314t-win32.whl", hash = "sha256:e846ae7835bf0703ae43f534ab79a867146dadd59dc9ca5c8b53d5c8f7c9ef02", size = 1955575, upload-time = "2026-05-06T13:38:51.116Z" },
+ { url = "https://files.pythonhosted.org/packages/40/1f/f1eb9eb350e795d1af8586289746f5c5677d16043040d63710e22abc43c9/pydantic_core-2.46.4-cp314-cp314t-win_amd64.whl", hash = "sha256:2108ba5c1c1eca18030634489dc544844144ee36357f2f9f780b93e7ddbb44b5", size = 2051624, upload-time = "2026-05-06T13:38:21.672Z" },
+ { url = "https://files.pythonhosted.org/packages/f6/d2/42dd53d0a85c27606f316d3aa5d2869c4e8470a5ed6dec30e4a1abe19192/pydantic_core-2.46.4-cp314-cp314t-win_arm64.whl", hash = "sha256:4fcbe087dbc2068af7eda3aa87634eba216dbda64d1ae73c8684b621d33f6596", size = 2017325, upload-time = "2026-05-06T13:40:52.723Z" },
]
[[package]]
@@ -1011,7 +1012,7 @@ version = "2.20.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" },
+ { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" },
]
[[package]]
@@ -1019,15 +1020,15 @@ name = "pytest"
version = "9.0.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
- { name = "colorama", marker = "sys_platform == 'win32'" },
- { name = "iniconfig" },
- { name = "packaging" },
- { name = "pluggy" },
- { name = "pygments" },
+ { name = "colorama", marker = "sys_platform == 'win32'" },
+ { name = "iniconfig" },
+ { name = "packaging" },
+ { name = "pluggy" },
+ { name = "pygments" },
]
sdist = { url = "https://files.pythonhosted.org/packages/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165, upload-time = "2026-04-07T17:16:18.027Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" },
+ { url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" },
]
[[package]]
@@ -1035,13 +1036,13 @@ name = "pytest-cov"
version = "7.1.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
- { name = "coverage" },
- { name = "pluggy" },
- { name = "pytest" },
+ { name = "coverage" },
+ { name = "pluggy" },
+ { name = "pytest" },
]
sdist = { url = "https://files.pythonhosted.org/packages/b1/51/a849f96e117386044471c8ec2bd6cfebacda285da9525c9106aeb28da671/pytest_cov-7.1.0.tar.gz", hash = "sha256:30674f2b5f6351aa09702a9c8c364f6a01c27aae0c1366ae8016160d1efc56b2", size = 55592, upload-time = "2026-03-21T20:11:16.284Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl", hash = "sha256:a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678", size = 22876, upload-time = "2026-03-21T20:11:14.438Z" },
+ { url = "https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl", hash = "sha256:a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678", size = 22876, upload-time = "2026-03-21T20:11:14.438Z" },
]
[[package]]
@@ -1049,11 +1050,11 @@ name = "python-dateutil"
version = "2.9.0.post0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
- { name = "six" },
+ { name = "six" },
]
sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" },
+ { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" },
]
[[package]]
@@ -1061,12 +1062,12 @@ name = "python-discovery"
version = "1.4.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
- { name = "filelock" },
- { name = "platformdirs" },
+ { name = "filelock" },
+ { name = "platformdirs" },
]
sdist = { url = "https://files.pythonhosted.org/packages/a6/12/38c1a0b1e64806780c9563e3fc9f6e472251839662587cfbe9bfaf2ae10a/python_discovery-1.4.0.tar.gz", hash = "sha256:eb8bc7daad3c226c147e45bb4e970a1feb1bf4048ee178e6db59e197b8010ce3", size = 68455, upload-time = "2026-05-28T01:15:37.639Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/c8/8d/3d316429f65029532bb1e28ff77b797d86b5ac3915bb44ca4e19aa283d43/python_discovery-1.4.0-py3-none-any.whl", hash = "sha256:26ed78d703e234879a66244c7d4114563fb13ec5cd30a2d1357e5fb4850782da", size = 33217, upload-time = "2026-05-28T01:15:36.573Z" },
+ { url = "https://files.pythonhosted.org/packages/c8/8d/3d316429f65029532bb1e28ff77b797d86b5ac3915bb44ca4e19aa283d43/python_discovery-1.4.0-py3-none-any.whl", hash = "sha256:26ed78d703e234879a66244c7d4114563fb13ec5cd30a2d1357e5fb4850782da", size = 33217, upload-time = "2026-05-28T01:15:36.573Z" },
]
[[package]]
@@ -1075,7 +1076,7 @@ version = "1.2.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/82/ed/0301aeeac3e5353ef3d94b6ec08bbcabd04a72018415dcb29e588514bba8/python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3", size = 50135, upload-time = "2026-03-01T16:00:26.196Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", size = 22101, upload-time = "2026-03-01T16:00:25.09Z" },
+ { url = "https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", size = 22101, upload-time = "2026-03-01T16:00:25.09Z" },
]
[[package]]
@@ -1084,34 +1085,34 @@ version = "6.0.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" },
- { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" },
- { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" },
- { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" },
- { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" },
- { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" },
- { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" },
- { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" },
- { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" },
- { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" },
- { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" },
- { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" },
- { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" },
- { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" },
- { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" },
- { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" },
- { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" },
- { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" },
- { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" },
- { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" },
- { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" },
- { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" },
- { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" },
- { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" },
- { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" },
- { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" },
- { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" },
- { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" },
+ { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" },
+ { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" },
+ { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" },
+ { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" },
+ { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" },
+ { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" },
+ { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" },
+ { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" },
+ { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" },
+ { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" },
+ { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" },
+ { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" },
+ { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" },
+ { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" },
+ { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" },
+ { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" },
+ { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" },
+ { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" },
+ { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" },
+ { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" },
+ { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" },
+ { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" },
+ { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" },
]
[[package]]
@@ -1120,70 +1121,70 @@ version = "2026.5.9"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/dc/0e/49aee608ad09480e7fd276898c99ec6192985fa331abe4eb3a986094490b/regex-2026.5.9.tar.gz", hash = "sha256:a8234aa23ec39894bfe4a3f1b85616a7032481964a13ac6fc9f10de4f6fca270", size = 416074, upload-time = "2026-05-09T23:15:19.37Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/aa/da/797e91ecec6f84135da778ddce78c20e0af5d2a15c26f87a81bc3eadb6db/regex-2026.5.9-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:d626b84406444b165fc0ba981604edea39f0588ff1f92baa23fe50799ea9afdb", size = 490303, upload-time = "2026-05-09T23:13:04.382Z" },
- { url = "https://files.pythonhosted.org/packages/44/da/bf30abaaa737b58f4a4b8c4a03659e02fd92092c822e0197ed9e0daab917/regex-2026.5.9-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d7bdc0ab8f3dd7e1b4f9ab88634e13374669db86bb3c72e8292f07ae313f539f", size = 292019, upload-time = "2026-05-09T23:13:06.022Z" },
- { url = "https://files.pythonhosted.org/packages/2d/e7/d0eaf5713828417b9e5648cf81fa9bacd4961f6ab98c380c2034f8716e35/regex-2026.5.9-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a8820737949116ffff55fe18f9fc644530063ba6ebfcb8314239416e78f1347c", size = 289468, upload-time = "2026-05-09T23:13:08.214Z" },
- { url = "https://files.pythonhosted.org/packages/d3/9b/b3fdd62b003baa1a9b593cd8c8699c9651c2e80cc21a5c715707983c42d7/regex-2026.5.9-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa0fbdbac82cb3e4450d0ccde7d7a35607f4cb2dd9fba4b8b69bfaf8c9fa6aed", size = 796749, upload-time = "2026-05-09T23:13:10.573Z" },
- { url = "https://files.pythonhosted.org/packages/d4/30/66ab84588765f5b4b271a9ca09ef7ce2b87caa95176ec3d2ad65d7bc4902/regex-2026.5.9-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:57e8915c7986aa33d25e4d3629cef711cd2863f2961b10409f0c04cb8b7d9020", size = 865445, upload-time = "2026-05-09T23:13:12.523Z" },
- { url = "https://files.pythonhosted.org/packages/1a/89/f05169e8588aac365f35ffc7f3bc3184f095ef4cfded7cfaa3c7fd5dbd89/regex-2026.5.9-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:508f56a89ba9cb26e4168cbc37dbd60a28d82430a9e18ad1d25fe0883c314ca2", size = 912322, upload-time = "2026-05-09T23:13:14.281Z" },
- { url = "https://files.pythonhosted.org/packages/30/e1/c93444052cf41581f3c884ab3fb5823daf0992f11cd4388d4275ca610558/regex-2026.5.9-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b6d189041f15691cfa2b6c4290448ec221244d225b3f5fe9e7771b34ffcdf6e2", size = 801269, upload-time = "2026-05-09T23:13:16.569Z" },
- { url = "https://files.pythonhosted.org/packages/50/fe/0cf96b882f540e62e8b9956599798203d599c44cf4c77917ca27400ff69b/regex-2026.5.9-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e82db382b44d0111b22601c509c89f64434816c9e0eef9d1989cda8cc6ff1c04", size = 777085, upload-time = "2026-05-09T23:13:18.675Z" },
- { url = "https://files.pythonhosted.org/packages/23/5c/d78d4924e7fc875557b9e9b768423925fdfaac5549d06da7810019a9bd26/regex-2026.5.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2acfb48634f64996b57f90f39afa692ff362162722581921fe92239a59960f3c", size = 785153, upload-time = "2026-05-09T23:13:20.525Z" },
- { url = "https://files.pythonhosted.org/packages/bf/e0/5214774090e7b4524dcea3e3c4aa74141d43043f8beb49c1599db1c8b53a/regex-2026.5.9-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:d29eebfc9525db68cad3c97eedd7f754fa265aa5cd0cf4f863b2421e1b48fc9f", size = 860164, upload-time = "2026-05-09T23:13:22.263Z" },
- { url = "https://files.pythonhosted.org/packages/6e/e1/4a57a83350319b1271f0d7a249b8672513ed928b237a741631270de6caea/regex-2026.5.9-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:debb893095e944091c16e641a6e33c1b0f4cb61ab945ec5afbf53ce7068834d8", size = 765731, upload-time = "2026-05-09T23:13:24.277Z" },
- { url = "https://files.pythonhosted.org/packages/12/f4/499e74a20c156fc75836ee04a72a38d1a063978f600937f9760467beb1b0/regex-2026.5.9-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:d659eee77986549c9ea45b861c7567e44d6287c3dc9a4565478853f7b9fe2ff6", size = 852062, upload-time = "2026-05-09T23:13:26.125Z" },
- { url = "https://files.pythonhosted.org/packages/5b/92/7eebc0d0a01e78629695f342ba17e0deaff8fb45e79cc0d7b98287da6e3e/regex-2026.5.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2efa205e6d98b24d1f3ab395c11aa15cdf10935bca283d0285e0499c284fba21", size = 789577, upload-time = "2026-05-09T23:13:27.814Z" },
- { url = "https://files.pythonhosted.org/packages/05/a4/018e71f7d2ad48c1ebe6d3ae0026f9b7cb4802fd15c7cc02fdf724355102/regex-2026.5.9-cp313-cp313-win32.whl", hash = "sha256:f3844f134e834076677dd369976e9f5068679fcb8e50102fdf6b7ac96a3ec127", size = 266691, upload-time = "2026-05-09T23:13:29.549Z" },
- { url = "https://files.pythonhosted.org/packages/e6/1d/861a93719fb9ee7dbfc3761b3797b7a3e112a5d42c6129459d2d741be9b5/regex-2026.5.9-cp313-cp313-win_amd64.whl", hash = "sha256:3527bb4942d2c14552155406cdedd906567456821848aed1cb4933a391bf5eca", size = 277747, upload-time = "2026-05-09T23:13:31.859Z" },
- { url = "https://files.pythonhosted.org/packages/d9/c6/0a2436ae4da1ba76e51cb98943c6838a9a721faa40ebe2dce07694ae34e3/regex-2026.5.9-cp313-cp313-win_arm64.whl", hash = "sha256:56a33f191f17d8c417f99945ebdc1e691d3af9605d86ec68c7e54a57e3e17af6", size = 270500, upload-time = "2026-05-09T23:13:33.525Z" },
- { url = "https://files.pythonhosted.org/packages/e8/e9/d21346f7b60ed58789371358ed66b09d00f832e1bd7c06e55d9da5679882/regex-2026.5.9-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:01f28d868834624c934b8d2e0aa1c8341337e37831f4a012f18a5afcba4cbaf3", size = 494172, upload-time = "2026-05-09T23:13:35.935Z" },
- { url = "https://files.pythonhosted.org/packages/c4/43/fd1177a2032037c681baecdb3422ee4e1424aec4e4f470ef47793d325274/regex-2026.5.9-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:48036f6374aaa79eb3b754ec29c61d1c6b1606749d705a13f8854fa2539671f6", size = 293952, upload-time = "2026-05-09T23:13:38.307Z" },
- { url = "https://files.pythonhosted.org/packages/f2/7d/9fbf919768368d3f8a4f6c692cf2aa61e482b2b81ec6a298ace4cbf02480/regex-2026.5.9-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b96350aa424e79d4fd6b567b344dcbe2b2d6bfc48dfe7717587e1fa6d43da6ff", size = 292314, upload-time = "2026-05-09T23:13:40.353Z" },
- { url = "https://files.pythonhosted.org/packages/e2/6c/e41bfeecb589716843e7c4df09ba46ff2a42961457afece19059d85caeef/regex-2026.5.9-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8f3af7a4903c5c04a11a196a5aa75cdd7dd3f8508132f9fb3259d9f5908e3b88", size = 811681, upload-time = "2026-05-09T23:13:42.543Z" },
- { url = "https://files.pythonhosted.org/packages/87/83/a5c1c525fba0aa656e88ad0face0b1829788ef4c2fb6b26df58aa1151b84/regex-2026.5.9-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7e87577720152d2caae19fe2baaf1f8d5ca12091e9e229f03915c37d1e4b9178", size = 871135, upload-time = "2026-05-09T23:13:44.326Z" },
- { url = "https://files.pythonhosted.org/packages/18/d4/80882e799e440dd878b0979cbebf8fa4d54624a332c83037c7a701649e3f/regex-2026.5.9-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c8b9b9d294cfea3cd19c718ade7cc93492b2c4991abd9a68d0b3477ae6d8e100", size = 917265, upload-time = "2026-05-09T23:13:47.295Z" },
- { url = "https://files.pythonhosted.org/packages/ae/ff/8db60211e2286e396aad7dc7725356c502bff0901ea05bd6cdc2e1a042b9/regex-2026.5.9-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:728d8bfd28a8845c8b6bc5dc7ce010453d206396786c0765c2740cb65f37791e", size = 816311, upload-time = "2026-05-09T23:13:49.885Z" },
- { url = "https://files.pythonhosted.org/packages/4c/47/742ef579c61730f8d268e5cf1f9ce0e37e2ea041ad0f5644724f2378e463/regex-2026.5.9-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7e30b874d341fac767d7df5a0870540541c2c054b80cfaac116e8d367a8a7ff2", size = 785498, upload-time = "2026-05-09T23:13:52.25Z" },
- { url = "https://files.pythonhosted.org/packages/7f/ab/cb0999802dcb0fb95b1ab005e8d4163d8afdd67efc2cb6b6630ac13f8cb1/regex-2026.5.9-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:fd190e88a895a8901325fad284a3f74ea52b1da8525b76cc811fa9b1edf0ce2b", size = 801348, upload-time = "2026-05-09T23:13:54.127Z" },
- { url = "https://files.pythonhosted.org/packages/7d/62/8ca59a24c55bc34d166eefaf3717bd77772f329fdbf984d86581e0a3571c/regex-2026.5.9-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:8e76e8161ad00694cfce6767d5dea860c6391ac5b83e5c3a39661e696f11fc7e", size = 866493, upload-time = "2026-05-09T23:13:56.067Z" },
- { url = "https://files.pythonhosted.org/packages/8d/3d/30f2ae62cef3278bb5bb821f467277a55fb73f01032cf85997e15e8289a8/regex-2026.5.9-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:ddda5340e6c01a293027dd46232fa79eaff1b48058ce7a98f572b6445b088041", size = 772811, upload-time = "2026-05-09T23:13:57.867Z" },
- { url = "https://files.pythonhosted.org/packages/d8/ae/7d2089bcd78ad0c0161bc684339df50032acb438a7bd3305e7ddb1193cec/regex-2026.5.9-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:205109e96b3cf5adf8f4cd62bedde9487feb282b9497a3535451e5a24cd706a0", size = 856584, upload-time = "2026-05-09T23:13:59.679Z" },
- { url = "https://files.pythonhosted.org/packages/a9/29/92ff47f75990131ea4f24ba17819e5a9d141e10819807e09addd73409af6/regex-2026.5.9-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dfbe4579b9f08036aa7d101d1835437a20783574ac66327e6b29b4018a138081", size = 803453, upload-time = "2026-05-09T23:14:01.978Z" },
- { url = "https://files.pythonhosted.org/packages/04/99/eff29f1037dcab36702c9ee5d6858cf1ce2336ea8ea2987f64245b99ea5e/regex-2026.5.9-cp313-cp313t-win32.whl", hash = "sha256:ed2c9e8068b614c574d8d30e543d617cf5379b0535d46f97ef00e904745a08b5", size = 269951, upload-time = "2026-05-09T23:14:03.661Z" },
- { url = "https://files.pythonhosted.org/packages/0e/9d/8870b8981d27b22cda77bb26a5ac7ebfa9c7d9e0dea195a834a82380e748/regex-2026.5.9-cp313-cp313t-win_amd64.whl", hash = "sha256:b46b0f094dc1d3b90356c85a0bd2c9bafc4a6a190b9d6f8ddd5a033b6e088ed4", size = 281240, upload-time = "2026-05-09T23:14:05.56Z" },
- { url = "https://files.pythonhosted.org/packages/72/b1/3379415e8f135c13ac551353397cc4fe97b4978f3cac73c5fcbcded548b8/regex-2026.5.9-cp313-cp313t-win_arm64.whl", hash = "sha256:872acc074bd29ffc9913ecdfedf6ea77502312ca44a4aa0d3779089c6069d8de", size = 272383, upload-time = "2026-05-09T23:14:07.843Z" },
- { url = "https://files.pythonhosted.org/packages/13/3e/9c3cd292d8808b3645a2ce517e200179b6d0e903f176300bd8b542e14de5/regex-2026.5.9-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:1bd7587a2948b4085195d5a3374eaf4a425dc3e55784c038175355ecf3bbbf8a", size = 490376, upload-time = "2026-05-09T23:14:09.64Z" },
- { url = "https://files.pythonhosted.org/packages/60/70/d43ee8a2ca0a8b68d167f21658b85520ac0574617c7f320367c5047f7556/regex-2026.5.9-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:dea2e88e1cce4522496cce630e11e67b98b7076620bc4336c3f674bc21a375f4", size = 291964, upload-time = "2026-05-09T23:14:11.424Z" },
- { url = "https://files.pythonhosted.org/packages/21/91/9d50b433828d8e74196904e168a43abf1e6e88b2a15d47ed742456720c37/regex-2026.5.9-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2099f7e7ff7b6aa3192312650a56e91cc091e49d50b04e4f6f8b6e28b3b27f1c", size = 289682, upload-time = "2026-05-09T23:14:13.123Z" },
- { url = "https://files.pythonhosted.org/packages/3e/d2/b835e3cafbb9d977736912436259ff551d60919f7d7b3d37d46659c63564/regex-2026.5.9-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ecd353045824e4477562a2ac718c25799cdaaa41f7aa925a806a8a3e6848a5b9", size = 796996, upload-time = "2026-05-09T23:14:14.923Z" },
- { url = "https://files.pythonhosted.org/packages/2c/a6/9f992d00019166b9de01c546dd4549bc679f2a68df11b877740b0760b7c2/regex-2026.5.9-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:65c8c8c37377794bd5b2f3ebe51919042bf17aec802e23c833d89782ed0c78af", size = 866089, upload-time = "2026-05-09T23:14:17.757Z" },
- { url = "https://files.pythonhosted.org/packages/e0/08/4d32af657e049b19cb62b02e46e38fe1518797bfb2203ee93a510b21b0dc/regex-2026.5.9-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5b73ab8afcf66c622db143d1c6fda4e58e4d537ee4f125229ad47b1ab80f34c0", size = 911530, upload-time = "2026-05-09T23:14:20.353Z" },
- { url = "https://files.pythonhosted.org/packages/d9/27/2af43dd1dc201d1fecefda64a45f4ad0995855b92724f795a777b402ee69/regex-2026.5.9-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0de5cf193997384ed2ca6f1cd4f78055b255d93d82d5a8cd6ba0d11c10b167e4", size = 800643, upload-time = "2026-05-09T23:14:22.265Z" },
- { url = "https://files.pythonhosted.org/packages/a4/dd/23a249047013b5321d4a60c4d2437462086f601b061776a525e5fba2a59f/regex-2026.5.9-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d641a8c9a61618047796d572a39a79b26167b0411d2c3031937b2fe2d081e2cf", size = 777223, upload-time = "2026-05-09T23:14:24.179Z" },
- { url = "https://files.pythonhosted.org/packages/94/6a/e85ed9538cd19586d0465076a4578a12e093ce776d15f3f8ce92733a8dd6/regex-2026.5.9-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:24b2355ef5cc9aa5b8f07d17704face1c166fdcc2290fa7bd6e6c925655a8346", size = 785760, upload-time = "2026-05-09T23:14:26.065Z" },
- { url = "https://files.pythonhosted.org/packages/2a/c4/f25473209438638e947c55f9156fd8f236f74169229028cc99116380868e/regex-2026.5.9-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:a24852d3c29ad9e47593593d8a247c44ccc3d0548ef12c822d6ed0810affe676", size = 860891, upload-time = "2026-05-09T23:14:28.17Z" },
- { url = "https://files.pythonhosted.org/packages/f9/f7/f4f86e3c74419c37370e91f150ae0c2ef7d34b2e0e4cdd5da046a02e4022/regex-2026.5.9-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:916714069da19329ef7de197dcbc77bb3104145c7c2c864dbfbe318f46b88b14", size = 765891, upload-time = "2026-05-09T23:14:30.06Z" },
- { url = "https://files.pythonhosted.org/packages/26/70/704d8e13765939146b1cd0ef4e2feb71d7929727d2290f026eed10095955/regex-2026.5.9-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:fa411799ca8da32a8d38d020a88faa5b6f91657d284761352940ecf9f7c3bbdd", size = 851380, upload-time = "2026-05-09T23:14:32.123Z" },
- { url = "https://files.pythonhosted.org/packages/26/29/1a13582a8460038edc38e49f64ceb0dd7c60f5caba77571f4bf6601965d9/regex-2026.5.9-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1e6da47d679b7010ef27556b6e0f99771b744936db1792a10ceac6547ae1503e", size = 789350, upload-time = "2026-05-09T23:14:34.799Z" },
- { url = "https://files.pythonhosted.org/packages/73/56/3dcafe34fc72e271d62ad9a291801e88a1457bb251c132f15fcc2e5aad1a/regex-2026.5.9-cp314-cp314-win32.whl", hash = "sha256:98bd73080e8756255137e1bd3f3f00295bbc5aa383c0e0f973920e9134d7c4ad", size = 272130, upload-time = "2026-05-09T23:14:36.729Z" },
- { url = "https://files.pythonhosted.org/packages/d0/9c/02eebf0be95efe416c664db7fb8b6b05b7a0b06a7544f2884f2558b0526f/regex-2026.5.9-cp314-cp314-win_amd64.whl", hash = "sha256:ff8d372ac2acdc048d1c19916f27ee61bc5722728458ba6ca5052f2c72d51763", size = 280999, upload-time = "2026-05-09T23:14:39.126Z" },
- { url = "https://files.pythonhosted.org/packages/70/5a/1dd1abee76cb7a846a0bcf42fdc87e5720c3c33c24f3e37814310a513d9f/regex-2026.5.9-cp314-cp314-win_arm64.whl", hash = "sha256:e1d93bf647916292e8edcec150c07ddf3dc50179ccaf770c04a7f9e452155372", size = 273500, upload-time = "2026-05-09T23:14:41.059Z" },
- { url = "https://files.pythonhosted.org/packages/86/c1/c5f619b0057a7965cb78ec559c1d7a45ce8c99a35bea95483d64959a93d9/regex-2026.5.9-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:83d0ee4a57d1c87cb549e195ec300b8f0ec3a82eba66d835e4e2ed8634fe4499", size = 494269, upload-time = "2026-05-09T23:14:42.869Z" },
- { url = "https://files.pythonhosted.org/packages/05/2c/5d01f1aee33de4bbe60c8452945bfc8477ca7c5ae4450f6bfe711036cb36/regex-2026.5.9-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:d3d7eb5c9a7f6df82ed3cfac9beb93882a5cbcb5b8b157b56cb2b3b276574ac1", size = 293954, upload-time = "2026-05-09T23:14:44.822Z" },
- { url = "https://files.pythonhosted.org/packages/7a/fe/e8988b2ae2108c6ef71bd4aa8d87fbe257976dd0810e826cd75f701c68b6/regex-2026.5.9-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:075160bf16658e16d35233300b8453aac25de4cbea808d22348b6979668e924d", size = 292405, upload-time = "2026-05-09T23:14:47.211Z" },
- { url = "https://files.pythonhosted.org/packages/79/34/d2b0937faa7859263f7f0a3c6b103a1296306be6952dc173d0154e9a2f49/regex-2026.5.9-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:45375819235558a4ff1c4971dc32881f022613abdb180128f5cb4768c1765a1c", size = 811855, upload-time = "2026-05-09T23:14:49.21Z" },
- { url = "https://files.pythonhosted.org/packages/80/fe/daf53a47457a8486db66c66c01ceb9c2303eecee3f87197f1e77eb1a736d/regex-2026.5.9-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ead4b163ac30a29574510cd4b3e2e985ac5290c05fc7095557d6a5f403fc31b5", size = 871189, upload-time = "2026-05-09T23:14:51.555Z" },
- { url = "https://files.pythonhosted.org/packages/1c/75/058fc4470cbfbf57d800aff1a0022b929a3f9fa553ee10a0cdf2070eb31f/regex-2026.5.9-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8c6e4218fbdfbcd4f6c19efca40930d24a621bf4b48cb76bc6640543bd28ef20", size = 917485, upload-time = "2026-05-09T23:14:53.633Z" },
- { url = "https://files.pythonhosted.org/packages/88/e7/179cfda3a28bc843b5c6cfe7f79f23489c791ed95f151083803660878432/regex-2026.5.9-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6351571c8a42b505eb555c0dc47d740d0fb66977dc142919eea6f4325b7c56a0", size = 816369, upload-time = "2026-05-09T23:14:56.198Z" },
- { url = "https://files.pythonhosted.org/packages/41/90/6f0cc422071688266d344fca8462d787cba0a2c144acb25721f9a61ec265/regex-2026.5.9-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:002205cafd2a9e78c6290c7d1df277bf3277b3b7a30e0b4bb0dac2e2e3f7cb2d", size = 785869, upload-time = "2026-05-09T23:14:58.602Z" },
- { url = "https://files.pythonhosted.org/packages/02/67/a31f1760f09c27b251ef39e9beb541f462cf977381d067faa764c2c0e393/regex-2026.5.9-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8abd33fef90b2a9efac5557d6033ca82d1195ed3a15fea5af15ba7b463c6a63b", size = 801427, upload-time = "2026-05-09T23:15:00.642Z" },
- { url = "https://files.pythonhosted.org/packages/e3/c4/1a80654597b6bc1e1ea0494824c31200e8a956abe290afae9b19a166a148/regex-2026.5.9-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:31037c82eccb44b7ea2e9e221d7c01429430e989a1f4b91ea5a855f6017b509a", size = 866482, upload-time = "2026-05-09T23:15:03.384Z" },
- { url = "https://files.pythonhosted.org/packages/d1/11/960724e06482c08466ff5611e242e86f80062949cdf6b4b9cc317b9dd93d/regex-2026.5.9-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:5604dfd046dc37eca90250fc3be938b076c8059fa772ac0ed6f499b0f0fb0415", size = 773022, upload-time = "2026-05-09T23:15:05.625Z" },
- { url = "https://files.pythonhosted.org/packages/50/a8/a9979c3e7918280e93159ebcab5ef1a65116dd4f3bd6091be0eae4a126e8/regex-2026.5.9-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:0e1b1b4e496afbb24f4a62aba855ee4f88f25578927697b340702e48c9ee6bc2", size = 856642, upload-time = "2026-05-09T23:15:07.966Z" },
- { url = "https://files.pythonhosted.org/packages/fe/d4/a9b732f2f0072c0ab12227483abb24fffcb9f73f8a2b203df0a6d0434735/regex-2026.5.9-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:be3372b9df6ddecff6486d37e19095a7b4973137caf5512407a89f4455361f41", size = 803552, upload-time = "2026-05-09T23:15:10.215Z" },
- { url = "https://files.pythonhosted.org/packages/d5/fe/1b3113817447a1d4155e4ac76d2e072f42c0bcba2f43fa8a0e756ea2cd91/regex-2026.5.9-cp314-cp314t-win32.whl", hash = "sha256:3ddd90103f9e5c471c49c7852ecc1fe27c7e45eb99e977aefe7caa4e779f4f58", size = 275746, upload-time = "2026-05-09T23:15:12.609Z" },
- { url = "https://files.pythonhosted.org/packages/92/73/93d42045302636c91f2e5ef588b65b84b01428f28ec77de256b1dfdfbe5c/regex-2026.5.9-cp314-cp314t-win_amd64.whl", hash = "sha256:ca518ed29c46eecba6010b15f1b9a479314d2de409536e71b6a13aa04e3b8a77", size = 285685, upload-time = "2026-05-09T23:15:15.086Z" },
- { url = "https://files.pythonhosted.org/packages/da/80/35b4c33c804a165a7f55289afda3ea9e3eb6d15800341a2d66455c0f1f30/regex-2026.5.9-cp314-cp314t-win_arm64.whl", hash = "sha256:5e41809d2683fcde7d5a8c87a6567ba1fb1ce0de9f31bff578de00a4b2d76daa", size = 275713, upload-time = "2026-05-09T23:15:16.98Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/da/797e91ecec6f84135da778ddce78c20e0af5d2a15c26f87a81bc3eadb6db/regex-2026.5.9-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:d626b84406444b165fc0ba981604edea39f0588ff1f92baa23fe50799ea9afdb", size = 490303, upload-time = "2026-05-09T23:13:04.382Z" },
+ { url = "https://files.pythonhosted.org/packages/44/da/bf30abaaa737b58f4a4b8c4a03659e02fd92092c822e0197ed9e0daab917/regex-2026.5.9-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d7bdc0ab8f3dd7e1b4f9ab88634e13374669db86bb3c72e8292f07ae313f539f", size = 292019, upload-time = "2026-05-09T23:13:06.022Z" },
+ { url = "https://files.pythonhosted.org/packages/2d/e7/d0eaf5713828417b9e5648cf81fa9bacd4961f6ab98c380c2034f8716e35/regex-2026.5.9-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a8820737949116ffff55fe18f9fc644530063ba6ebfcb8314239416e78f1347c", size = 289468, upload-time = "2026-05-09T23:13:08.214Z" },
+ { url = "https://files.pythonhosted.org/packages/d3/9b/b3fdd62b003baa1a9b593cd8c8699c9651c2e80cc21a5c715707983c42d7/regex-2026.5.9-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa0fbdbac82cb3e4450d0ccde7d7a35607f4cb2dd9fba4b8b69bfaf8c9fa6aed", size = 796749, upload-time = "2026-05-09T23:13:10.573Z" },
+ { url = "https://files.pythonhosted.org/packages/d4/30/66ab84588765f5b4b271a9ca09ef7ce2b87caa95176ec3d2ad65d7bc4902/regex-2026.5.9-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:57e8915c7986aa33d25e4d3629cef711cd2863f2961b10409f0c04cb8b7d9020", size = 865445, upload-time = "2026-05-09T23:13:12.523Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/89/f05169e8588aac365f35ffc7f3bc3184f095ef4cfded7cfaa3c7fd5dbd89/regex-2026.5.9-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:508f56a89ba9cb26e4168cbc37dbd60a28d82430a9e18ad1d25fe0883c314ca2", size = 912322, upload-time = "2026-05-09T23:13:14.281Z" },
+ { url = "https://files.pythonhosted.org/packages/30/e1/c93444052cf41581f3c884ab3fb5823daf0992f11cd4388d4275ca610558/regex-2026.5.9-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b6d189041f15691cfa2b6c4290448ec221244d225b3f5fe9e7771b34ffcdf6e2", size = 801269, upload-time = "2026-05-09T23:13:16.569Z" },
+ { url = "https://files.pythonhosted.org/packages/50/fe/0cf96b882f540e62e8b9956599798203d599c44cf4c77917ca27400ff69b/regex-2026.5.9-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e82db382b44d0111b22601c509c89f64434816c9e0eef9d1989cda8cc6ff1c04", size = 777085, upload-time = "2026-05-09T23:13:18.675Z" },
+ { url = "https://files.pythonhosted.org/packages/23/5c/d78d4924e7fc875557b9e9b768423925fdfaac5549d06da7810019a9bd26/regex-2026.5.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2acfb48634f64996b57f90f39afa692ff362162722581921fe92239a59960f3c", size = 785153, upload-time = "2026-05-09T23:13:20.525Z" },
+ { url = "https://files.pythonhosted.org/packages/bf/e0/5214774090e7b4524dcea3e3c4aa74141d43043f8beb49c1599db1c8b53a/regex-2026.5.9-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:d29eebfc9525db68cad3c97eedd7f754fa265aa5cd0cf4f863b2421e1b48fc9f", size = 860164, upload-time = "2026-05-09T23:13:22.263Z" },
+ { url = "https://files.pythonhosted.org/packages/6e/e1/4a57a83350319b1271f0d7a249b8672513ed928b237a741631270de6caea/regex-2026.5.9-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:debb893095e944091c16e641a6e33c1b0f4cb61ab945ec5afbf53ce7068834d8", size = 765731, upload-time = "2026-05-09T23:13:24.277Z" },
+ { url = "https://files.pythonhosted.org/packages/12/f4/499e74a20c156fc75836ee04a72a38d1a063978f600937f9760467beb1b0/regex-2026.5.9-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:d659eee77986549c9ea45b861c7567e44d6287c3dc9a4565478853f7b9fe2ff6", size = 852062, upload-time = "2026-05-09T23:13:26.125Z" },
+ { url = "https://files.pythonhosted.org/packages/5b/92/7eebc0d0a01e78629695f342ba17e0deaff8fb45e79cc0d7b98287da6e3e/regex-2026.5.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2efa205e6d98b24d1f3ab395c11aa15cdf10935bca283d0285e0499c284fba21", size = 789577, upload-time = "2026-05-09T23:13:27.814Z" },
+ { url = "https://files.pythonhosted.org/packages/05/a4/018e71f7d2ad48c1ebe6d3ae0026f9b7cb4802fd15c7cc02fdf724355102/regex-2026.5.9-cp313-cp313-win32.whl", hash = "sha256:f3844f134e834076677dd369976e9f5068679fcb8e50102fdf6b7ac96a3ec127", size = 266691, upload-time = "2026-05-09T23:13:29.549Z" },
+ { url = "https://files.pythonhosted.org/packages/e6/1d/861a93719fb9ee7dbfc3761b3797b7a3e112a5d42c6129459d2d741be9b5/regex-2026.5.9-cp313-cp313-win_amd64.whl", hash = "sha256:3527bb4942d2c14552155406cdedd906567456821848aed1cb4933a391bf5eca", size = 277747, upload-time = "2026-05-09T23:13:31.859Z" },
+ { url = "https://files.pythonhosted.org/packages/d9/c6/0a2436ae4da1ba76e51cb98943c6838a9a721faa40ebe2dce07694ae34e3/regex-2026.5.9-cp313-cp313-win_arm64.whl", hash = "sha256:56a33f191f17d8c417f99945ebdc1e691d3af9605d86ec68c7e54a57e3e17af6", size = 270500, upload-time = "2026-05-09T23:13:33.525Z" },
+ { url = "https://files.pythonhosted.org/packages/e8/e9/d21346f7b60ed58789371358ed66b09d00f832e1bd7c06e55d9da5679882/regex-2026.5.9-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:01f28d868834624c934b8d2e0aa1c8341337e37831f4a012f18a5afcba4cbaf3", size = 494172, upload-time = "2026-05-09T23:13:35.935Z" },
+ { url = "https://files.pythonhosted.org/packages/c4/43/fd1177a2032037c681baecdb3422ee4e1424aec4e4f470ef47793d325274/regex-2026.5.9-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:48036f6374aaa79eb3b754ec29c61d1c6b1606749d705a13f8854fa2539671f6", size = 293952, upload-time = "2026-05-09T23:13:38.307Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/7d/9fbf919768368d3f8a4f6c692cf2aa61e482b2b81ec6a298ace4cbf02480/regex-2026.5.9-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b96350aa424e79d4fd6b567b344dcbe2b2d6bfc48dfe7717587e1fa6d43da6ff", size = 292314, upload-time = "2026-05-09T23:13:40.353Z" },
+ { url = "https://files.pythonhosted.org/packages/e2/6c/e41bfeecb589716843e7c4df09ba46ff2a42961457afece19059d85caeef/regex-2026.5.9-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8f3af7a4903c5c04a11a196a5aa75cdd7dd3f8508132f9fb3259d9f5908e3b88", size = 811681, upload-time = "2026-05-09T23:13:42.543Z" },
+ { url = "https://files.pythonhosted.org/packages/87/83/a5c1c525fba0aa656e88ad0face0b1829788ef4c2fb6b26df58aa1151b84/regex-2026.5.9-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7e87577720152d2caae19fe2baaf1f8d5ca12091e9e229f03915c37d1e4b9178", size = 871135, upload-time = "2026-05-09T23:13:44.326Z" },
+ { url = "https://files.pythonhosted.org/packages/18/d4/80882e799e440dd878b0979cbebf8fa4d54624a332c83037c7a701649e3f/regex-2026.5.9-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c8b9b9d294cfea3cd19c718ade7cc93492b2c4991abd9a68d0b3477ae6d8e100", size = 917265, upload-time = "2026-05-09T23:13:47.295Z" },
+ { url = "https://files.pythonhosted.org/packages/ae/ff/8db60211e2286e396aad7dc7725356c502bff0901ea05bd6cdc2e1a042b9/regex-2026.5.9-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:728d8bfd28a8845c8b6bc5dc7ce010453d206396786c0765c2740cb65f37791e", size = 816311, upload-time = "2026-05-09T23:13:49.885Z" },
+ { url = "https://files.pythonhosted.org/packages/4c/47/742ef579c61730f8d268e5cf1f9ce0e37e2ea041ad0f5644724f2378e463/regex-2026.5.9-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7e30b874d341fac767d7df5a0870540541c2c054b80cfaac116e8d367a8a7ff2", size = 785498, upload-time = "2026-05-09T23:13:52.25Z" },
+ { url = "https://files.pythonhosted.org/packages/7f/ab/cb0999802dcb0fb95b1ab005e8d4163d8afdd67efc2cb6b6630ac13f8cb1/regex-2026.5.9-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:fd190e88a895a8901325fad284a3f74ea52b1da8525b76cc811fa9b1edf0ce2b", size = 801348, upload-time = "2026-05-09T23:13:54.127Z" },
+ { url = "https://files.pythonhosted.org/packages/7d/62/8ca59a24c55bc34d166eefaf3717bd77772f329fdbf984d86581e0a3571c/regex-2026.5.9-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:8e76e8161ad00694cfce6767d5dea860c6391ac5b83e5c3a39661e696f11fc7e", size = 866493, upload-time = "2026-05-09T23:13:56.067Z" },
+ { url = "https://files.pythonhosted.org/packages/8d/3d/30f2ae62cef3278bb5bb821f467277a55fb73f01032cf85997e15e8289a8/regex-2026.5.9-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:ddda5340e6c01a293027dd46232fa79eaff1b48058ce7a98f572b6445b088041", size = 772811, upload-time = "2026-05-09T23:13:57.867Z" },
+ { url = "https://files.pythonhosted.org/packages/d8/ae/7d2089bcd78ad0c0161bc684339df50032acb438a7bd3305e7ddb1193cec/regex-2026.5.9-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:205109e96b3cf5adf8f4cd62bedde9487feb282b9497a3535451e5a24cd706a0", size = 856584, upload-time = "2026-05-09T23:13:59.679Z" },
+ { url = "https://files.pythonhosted.org/packages/a9/29/92ff47f75990131ea4f24ba17819e5a9d141e10819807e09addd73409af6/regex-2026.5.9-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dfbe4579b9f08036aa7d101d1835437a20783574ac66327e6b29b4018a138081", size = 803453, upload-time = "2026-05-09T23:14:01.978Z" },
+ { url = "https://files.pythonhosted.org/packages/04/99/eff29f1037dcab36702c9ee5d6858cf1ce2336ea8ea2987f64245b99ea5e/regex-2026.5.9-cp313-cp313t-win32.whl", hash = "sha256:ed2c9e8068b614c574d8d30e543d617cf5379b0535d46f97ef00e904745a08b5", size = 269951, upload-time = "2026-05-09T23:14:03.661Z" },
+ { url = "https://files.pythonhosted.org/packages/0e/9d/8870b8981d27b22cda77bb26a5ac7ebfa9c7d9e0dea195a834a82380e748/regex-2026.5.9-cp313-cp313t-win_amd64.whl", hash = "sha256:b46b0f094dc1d3b90356c85a0bd2c9bafc4a6a190b9d6f8ddd5a033b6e088ed4", size = 281240, upload-time = "2026-05-09T23:14:05.56Z" },
+ { url = "https://files.pythonhosted.org/packages/72/b1/3379415e8f135c13ac551353397cc4fe97b4978f3cac73c5fcbcded548b8/regex-2026.5.9-cp313-cp313t-win_arm64.whl", hash = "sha256:872acc074bd29ffc9913ecdfedf6ea77502312ca44a4aa0d3779089c6069d8de", size = 272383, upload-time = "2026-05-09T23:14:07.843Z" },
+ { url = "https://files.pythonhosted.org/packages/13/3e/9c3cd292d8808b3645a2ce517e200179b6d0e903f176300bd8b542e14de5/regex-2026.5.9-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:1bd7587a2948b4085195d5a3374eaf4a425dc3e55784c038175355ecf3bbbf8a", size = 490376, upload-time = "2026-05-09T23:14:09.64Z" },
+ { url = "https://files.pythonhosted.org/packages/60/70/d43ee8a2ca0a8b68d167f21658b85520ac0574617c7f320367c5047f7556/regex-2026.5.9-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:dea2e88e1cce4522496cce630e11e67b98b7076620bc4336c3f674bc21a375f4", size = 291964, upload-time = "2026-05-09T23:14:11.424Z" },
+ { url = "https://files.pythonhosted.org/packages/21/91/9d50b433828d8e74196904e168a43abf1e6e88b2a15d47ed742456720c37/regex-2026.5.9-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2099f7e7ff7b6aa3192312650a56e91cc091e49d50b04e4f6f8b6e28b3b27f1c", size = 289682, upload-time = "2026-05-09T23:14:13.123Z" },
+ { url = "https://files.pythonhosted.org/packages/3e/d2/b835e3cafbb9d977736912436259ff551d60919f7d7b3d37d46659c63564/regex-2026.5.9-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ecd353045824e4477562a2ac718c25799cdaaa41f7aa925a806a8a3e6848a5b9", size = 796996, upload-time = "2026-05-09T23:14:14.923Z" },
+ { url = "https://files.pythonhosted.org/packages/2c/a6/9f992d00019166b9de01c546dd4549bc679f2a68df11b877740b0760b7c2/regex-2026.5.9-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:65c8c8c37377794bd5b2f3ebe51919042bf17aec802e23c833d89782ed0c78af", size = 866089, upload-time = "2026-05-09T23:14:17.757Z" },
+ { url = "https://files.pythonhosted.org/packages/e0/08/4d32af657e049b19cb62b02e46e38fe1518797bfb2203ee93a510b21b0dc/regex-2026.5.9-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5b73ab8afcf66c622db143d1c6fda4e58e4d537ee4f125229ad47b1ab80f34c0", size = 911530, upload-time = "2026-05-09T23:14:20.353Z" },
+ { url = "https://files.pythonhosted.org/packages/d9/27/2af43dd1dc201d1fecefda64a45f4ad0995855b92724f795a777b402ee69/regex-2026.5.9-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0de5cf193997384ed2ca6f1cd4f78055b255d93d82d5a8cd6ba0d11c10b167e4", size = 800643, upload-time = "2026-05-09T23:14:22.265Z" },
+ { url = "https://files.pythonhosted.org/packages/a4/dd/23a249047013b5321d4a60c4d2437462086f601b061776a525e5fba2a59f/regex-2026.5.9-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d641a8c9a61618047796d572a39a79b26167b0411d2c3031937b2fe2d081e2cf", size = 777223, upload-time = "2026-05-09T23:14:24.179Z" },
+ { url = "https://files.pythonhosted.org/packages/94/6a/e85ed9538cd19586d0465076a4578a12e093ce776d15f3f8ce92733a8dd6/regex-2026.5.9-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:24b2355ef5cc9aa5b8f07d17704face1c166fdcc2290fa7bd6e6c925655a8346", size = 785760, upload-time = "2026-05-09T23:14:26.065Z" },
+ { url = "https://files.pythonhosted.org/packages/2a/c4/f25473209438638e947c55f9156fd8f236f74169229028cc99116380868e/regex-2026.5.9-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:a24852d3c29ad9e47593593d8a247c44ccc3d0548ef12c822d6ed0810affe676", size = 860891, upload-time = "2026-05-09T23:14:28.17Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/f7/f4f86e3c74419c37370e91f150ae0c2ef7d34b2e0e4cdd5da046a02e4022/regex-2026.5.9-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:916714069da19329ef7de197dcbc77bb3104145c7c2c864dbfbe318f46b88b14", size = 765891, upload-time = "2026-05-09T23:14:30.06Z" },
+ { url = "https://files.pythonhosted.org/packages/26/70/704d8e13765939146b1cd0ef4e2feb71d7929727d2290f026eed10095955/regex-2026.5.9-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:fa411799ca8da32a8d38d020a88faa5b6f91657d284761352940ecf9f7c3bbdd", size = 851380, upload-time = "2026-05-09T23:14:32.123Z" },
+ { url = "https://files.pythonhosted.org/packages/26/29/1a13582a8460038edc38e49f64ceb0dd7c60f5caba77571f4bf6601965d9/regex-2026.5.9-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1e6da47d679b7010ef27556b6e0f99771b744936db1792a10ceac6547ae1503e", size = 789350, upload-time = "2026-05-09T23:14:34.799Z" },
+ { url = "https://files.pythonhosted.org/packages/73/56/3dcafe34fc72e271d62ad9a291801e88a1457bb251c132f15fcc2e5aad1a/regex-2026.5.9-cp314-cp314-win32.whl", hash = "sha256:98bd73080e8756255137e1bd3f3f00295bbc5aa383c0e0f973920e9134d7c4ad", size = 272130, upload-time = "2026-05-09T23:14:36.729Z" },
+ { url = "https://files.pythonhosted.org/packages/d0/9c/02eebf0be95efe416c664db7fb8b6b05b7a0b06a7544f2884f2558b0526f/regex-2026.5.9-cp314-cp314-win_amd64.whl", hash = "sha256:ff8d372ac2acdc048d1c19916f27ee61bc5722728458ba6ca5052f2c72d51763", size = 280999, upload-time = "2026-05-09T23:14:39.126Z" },
+ { url = "https://files.pythonhosted.org/packages/70/5a/1dd1abee76cb7a846a0bcf42fdc87e5720c3c33c24f3e37814310a513d9f/regex-2026.5.9-cp314-cp314-win_arm64.whl", hash = "sha256:e1d93bf647916292e8edcec150c07ddf3dc50179ccaf770c04a7f9e452155372", size = 273500, upload-time = "2026-05-09T23:14:41.059Z" },
+ { url = "https://files.pythonhosted.org/packages/86/c1/c5f619b0057a7965cb78ec559c1d7a45ce8c99a35bea95483d64959a93d9/regex-2026.5.9-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:83d0ee4a57d1c87cb549e195ec300b8f0ec3a82eba66d835e4e2ed8634fe4499", size = 494269, upload-time = "2026-05-09T23:14:42.869Z" },
+ { url = "https://files.pythonhosted.org/packages/05/2c/5d01f1aee33de4bbe60c8452945bfc8477ca7c5ae4450f6bfe711036cb36/regex-2026.5.9-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:d3d7eb5c9a7f6df82ed3cfac9beb93882a5cbcb5b8b157b56cb2b3b276574ac1", size = 293954, upload-time = "2026-05-09T23:14:44.822Z" },
+ { url = "https://files.pythonhosted.org/packages/7a/fe/e8988b2ae2108c6ef71bd4aa8d87fbe257976dd0810e826cd75f701c68b6/regex-2026.5.9-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:075160bf16658e16d35233300b8453aac25de4cbea808d22348b6979668e924d", size = 292405, upload-time = "2026-05-09T23:14:47.211Z" },
+ { url = "https://files.pythonhosted.org/packages/79/34/d2b0937faa7859263f7f0a3c6b103a1296306be6952dc173d0154e9a2f49/regex-2026.5.9-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:45375819235558a4ff1c4971dc32881f022613abdb180128f5cb4768c1765a1c", size = 811855, upload-time = "2026-05-09T23:14:49.21Z" },
+ { url = "https://files.pythonhosted.org/packages/80/fe/daf53a47457a8486db66c66c01ceb9c2303eecee3f87197f1e77eb1a736d/regex-2026.5.9-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ead4b163ac30a29574510cd4b3e2e985ac5290c05fc7095557d6a5f403fc31b5", size = 871189, upload-time = "2026-05-09T23:14:51.555Z" },
+ { url = "https://files.pythonhosted.org/packages/1c/75/058fc4470cbfbf57d800aff1a0022b929a3f9fa553ee10a0cdf2070eb31f/regex-2026.5.9-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8c6e4218fbdfbcd4f6c19efca40930d24a621bf4b48cb76bc6640543bd28ef20", size = 917485, upload-time = "2026-05-09T23:14:53.633Z" },
+ { url = "https://files.pythonhosted.org/packages/88/e7/179cfda3a28bc843b5c6cfe7f79f23489c791ed95f151083803660878432/regex-2026.5.9-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6351571c8a42b505eb555c0dc47d740d0fb66977dc142919eea6f4325b7c56a0", size = 816369, upload-time = "2026-05-09T23:14:56.198Z" },
+ { url = "https://files.pythonhosted.org/packages/41/90/6f0cc422071688266d344fca8462d787cba0a2c144acb25721f9a61ec265/regex-2026.5.9-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:002205cafd2a9e78c6290c7d1df277bf3277b3b7a30e0b4bb0dac2e2e3f7cb2d", size = 785869, upload-time = "2026-05-09T23:14:58.602Z" },
+ { url = "https://files.pythonhosted.org/packages/02/67/a31f1760f09c27b251ef39e9beb541f462cf977381d067faa764c2c0e393/regex-2026.5.9-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8abd33fef90b2a9efac5557d6033ca82d1195ed3a15fea5af15ba7b463c6a63b", size = 801427, upload-time = "2026-05-09T23:15:00.642Z" },
+ { url = "https://files.pythonhosted.org/packages/e3/c4/1a80654597b6bc1e1ea0494824c31200e8a956abe290afae9b19a166a148/regex-2026.5.9-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:31037c82eccb44b7ea2e9e221d7c01429430e989a1f4b91ea5a855f6017b509a", size = 866482, upload-time = "2026-05-09T23:15:03.384Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/11/960724e06482c08466ff5611e242e86f80062949cdf6b4b9cc317b9dd93d/regex-2026.5.9-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:5604dfd046dc37eca90250fc3be938b076c8059fa772ac0ed6f499b0f0fb0415", size = 773022, upload-time = "2026-05-09T23:15:05.625Z" },
+ { url = "https://files.pythonhosted.org/packages/50/a8/a9979c3e7918280e93159ebcab5ef1a65116dd4f3bd6091be0eae4a126e8/regex-2026.5.9-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:0e1b1b4e496afbb24f4a62aba855ee4f88f25578927697b340702e48c9ee6bc2", size = 856642, upload-time = "2026-05-09T23:15:07.966Z" },
+ { url = "https://files.pythonhosted.org/packages/fe/d4/a9b732f2f0072c0ab12227483abb24fffcb9f73f8a2b203df0a6d0434735/regex-2026.5.9-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:be3372b9df6ddecff6486d37e19095a7b4973137caf5512407a89f4455361f41", size = 803552, upload-time = "2026-05-09T23:15:10.215Z" },
+ { url = "https://files.pythonhosted.org/packages/d5/fe/1b3113817447a1d4155e4ac76d2e072f42c0bcba2f43fa8a0e756ea2cd91/regex-2026.5.9-cp314-cp314t-win32.whl", hash = "sha256:3ddd90103f9e5c471c49c7852ecc1fe27c7e45eb99e977aefe7caa4e779f4f58", size = 275746, upload-time = "2026-05-09T23:15:12.609Z" },
+ { url = "https://files.pythonhosted.org/packages/92/73/93d42045302636c91f2e5ef588b65b84b01428f28ec77de256b1dfdfbe5c/regex-2026.5.9-cp314-cp314t-win_amd64.whl", hash = "sha256:ca518ed29c46eecba6010b15f1b9a479314d2de409536e71b6a13aa04e3b8a77", size = 285685, upload-time = "2026-05-09T23:15:15.086Z" },
+ { url = "https://files.pythonhosted.org/packages/da/80/35b4c33c804a165a7f55289afda3ea9e3eb6d15800341a2d66455c0f1f30/regex-2026.5.9-cp314-cp314t-win_arm64.whl", hash = "sha256:5e41809d2683fcde7d5a8c87a6567ba1fb1ce0de9f31bff578de00a4b2d76daa", size = 275713, upload-time = "2026-05-09T23:15:16.98Z" },
]
[[package]]
@@ -1191,14 +1192,14 @@ name = "requests"
version = "2.34.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
- { name = "certifi" },
- { name = "charset-normalizer" },
- { name = "idna" },
- { name = "urllib3" },
+ { name = "certifi" },
+ { name = "charset-normalizer" },
+ { name = "idna" },
+ { name = "urllib3" },
]
sdist = { url = "https://files.pythonhosted.org/packages/ac/c3/e2a2b89f2d3e2179abd6d00ebd70bff6273f37fb3e0cc209f48b39d00cbf/requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed", size = 142856, upload-time = "2026-05-14T19:25:27.735Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", size = 73075, upload-time = "2026-05-14T19:25:26.443Z" },
+ { url = "https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", size = 73075, upload-time = "2026-05-14T19:25:26.443Z" },
]
[[package]]
@@ -1206,12 +1207,12 @@ name = "rich"
version = "15.0.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
- { name = "markdown-it-py" },
- { name = "pygments" },
+ { name = "markdown-it-py" },
+ { name = "pygments" },
]
sdist = { url = "https://files.pythonhosted.org/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36", size = 230680, upload-time = "2026-04-12T08:24:00.75Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654, upload-time = "2026-04-12T08:24:02.83Z" },
+ { url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654, upload-time = "2026-04-12T08:24:02.83Z" },
]
[[package]]
@@ -1220,23 +1221,23 @@ version = "0.15.14"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/dc/8a/8bce2894573e9dae6ff4d77fe34ad727d79b9e6238ad288c5638990d90f6/ruff-0.15.14.tar.gz", hash = "sha256:48e866b165be4a9bdbf310f7d3c9a07edef2fe8cd63ffeb4e00bb590506ebf9f", size = 4700910, upload-time = "2026-05-21T14:34:55.177Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/b9/c8/74a92c6ff9fcfb4f1f947126d3ebee8389276e161ecc85de5bda7cda51bd/ruff-0.15.14-py3-none-linux_armv6l.whl", hash = "sha256:8dd2db9416e487c8d4b01fa7056bb02c4d05969d4f8d17a08c229c2f4ff3c108", size = 10739177, upload-time = "2026-05-21T14:34:37.332Z" },
- { url = "https://files.pythonhosted.org/packages/45/91/254a35c20acc38a7223c9d2d594af12e794432464f2cdeb52af1dc4a892d/ruff-0.15.14-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:be4ff55af755bd71a00ab3dc6bd7ffc467bd76e0df6881e286c2e3d23e8fb43b", size = 11144969, upload-time = "2026-05-21T14:34:43.978Z" },
- { url = "https://files.pythonhosted.org/packages/56/9e/d13e40f83b8d0a94430e6778ce1d94a43b38cf2efe63278bdd2b4c65abbf/ruff-0.15.14-py3-none-macosx_11_0_arm64.whl", hash = "sha256:48d5909d7d06276ce7dde6d32bfa4b0d4cb2651145cd8ee4b440722cbc77832f", size = 10478207, upload-time = "2026-05-21T14:34:48.378Z" },
- { url = "https://files.pythonhosted.org/packages/8d/f1/b15a7839fa4f332f8acec78e20564f26bb2d866e3d21710b877fd0263000/ruff-0.15.14-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca8cbfa94c4f90984a67561978602746d4cd27103568f745fa90eee3f0d4107d", size = 10818459, upload-time = "2026-05-21T14:34:22.318Z" },
- { url = "https://files.pythonhosted.org/packages/45/33/53d651177f84f94b400a0e27f8824eeada3dddc9d5ee8aeb048f4352a520/ruff-0.15.14-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9a6bbc0333f1ab053423bcbf6226477d266ca7cec7738c4c8e3f55647803f3c4", size = 10541800, upload-time = "2026-05-21T14:34:20.209Z" },
- { url = "https://files.pythonhosted.org/packages/b8/a6/868f87e0bf9786ed24b5d0d0ad8676b8a94fd1912f42cddf9cfc7857818a/ruff-0.15.14-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8a24a4f7605d7003a6674d4387651effd939dead3fddd0f36561eb77a9a2e542", size = 11342149, upload-time = "2026-05-21T14:34:46.365Z" },
- { url = "https://files.pythonhosted.org/packages/a7/8b/38cd5c19faffdcc05a408d2b78edccc69492ab9720eadb49ea15ef80d768/ruff-0.15.14-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:049b5326e53ed80978f2fc041a280603f69dd6b0c95464342a2bb4572d9d9e2f", size = 12212563, upload-time = "2026-05-21T14:34:28.579Z" },
- { url = "https://files.pythonhosted.org/packages/3e/4d/a3c5b874a556d5731e3e657aaf04311bb76f0a5c3ec220ed43051be6b64b/ruff-0.15.14-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d4ed42e6696c8dfa5f06728e6441993901f548eb92d73bc472cb5a38d1395fbf", size = 11493299, upload-time = "2026-05-21T14:34:41.836Z" },
- { url = "https://files.pythonhosted.org/packages/1e/c0/56472c251d09858a53e51efbd485b09e1995d8731668b76d52e5dd6ee0f1/ruff-0.15.14-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:715c543cf450c4888251f91c52f1942a800541d9bddd7ac060aa4e6b77ae7cba", size = 11455931, upload-time = "2026-05-21T14:34:57.276Z" },
- { url = "https://files.pythonhosted.org/packages/2c/4a/e2e7b4d8dbf233d4eace59c75bc3435fa6d8bd3bae82d351d4e4300c0fd1/ruff-0.15.14-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:72ebab6013ec887d439d8b7593737a0a4ffb06d45d209d4e4bf2e92813082d3f", size = 11400794, upload-time = "2026-05-21T14:34:39.773Z" },
- { url = "https://files.pythonhosted.org/packages/97/c7/83c0539fe34c3e09136204d1e75d6052492364e0b3cb05e9465423f567d7/ruff-0.15.14-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:49072d36abdbe97a8dd7f480afe9c675699c0c495d4c84076e2c1203c4550581", size = 10804759, upload-time = "2026-05-21T14:34:31.045Z" },
- { url = "https://files.pythonhosted.org/packages/86/a6/18f2bfc095a2ab4a78745644e428205532ce6653a5d0fa8501572891534d/ruff-0.15.14-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:958522aee105068640c2c2ceae08f413ae44d922f52a1374ac13d6a96032fc93", size = 10539517, upload-time = "2026-05-21T14:34:53.064Z" },
- { url = "https://files.pythonhosted.org/packages/54/3a/5a8b3b69c654d4e4bf1d246ac5b49cbcdac6eaab6905925f8915f31e3b80/ruff-0.15.14-py3-none-musllinux_1_2_i686.whl", hash = "sha256:f3707da619a143a2e8830e2abab8224478d69ace2d28cb6c20543ae97c36bf61", size = 11065169, upload-time = "2026-05-21T14:34:24.484Z" },
- { url = "https://files.pythonhosted.org/packages/ed/c5/8864e4e7925b836ea354b31d57641ec03830564e281a8b6f061f8c3e0ec1/ruff-0.15.14-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:bb01d645694e3ec0102105d07ef2d53703970407d59c04e59d3ba0b7a1d53553", size = 11560214, upload-time = "2026-05-21T14:34:50.975Z" },
- { url = "https://files.pythonhosted.org/packages/36/38/012bf76752e1f89ed50b77b99532d90f3a3e287bc7918e1fc0948ac866ac/ruff-0.15.14-py3-none-win32.whl", hash = "sha256:6d0c1ad2a0ab718d39b6d8fd2217981ce4d625cd96a720095f798fb47d8b13e6", size = 10805548, upload-time = "2026-05-21T14:34:33.453Z" },
- { url = "https://files.pythonhosted.org/packages/d1/b7/4ea2c170f10ad760fff2a5250beb18897719dc8b52b53a24cddbb9dd3f19/ruff-0.15.14-py3-none-win_amd64.whl", hash = "sha256:802342981e056db3851a7836e5b070f8f15f67d4a685ae2a6160939d364b2902", size = 11939523, upload-time = "2026-05-21T14:34:18.077Z" },
- { url = "https://files.pythonhosted.org/packages/62/d5/bc97ff895ec35cf3925d4bd60f3b39d822f377a446906ec9bcc87405e59b/ruff-0.15.14-py3-none-win_arm64.whl", hash = "sha256:ff47b90a9ef6a40c9e2f3b479c1fb78531adf055b94c1eba0a7ba04b31951826", size = 11208607, upload-time = "2026-05-21T14:34:26.525Z" },
+ { url = "https://files.pythonhosted.org/packages/b9/c8/74a92c6ff9fcfb4f1f947126d3ebee8389276e161ecc85de5bda7cda51bd/ruff-0.15.14-py3-none-linux_armv6l.whl", hash = "sha256:8dd2db9416e487c8d4b01fa7056bb02c4d05969d4f8d17a08c229c2f4ff3c108", size = 10739177, upload-time = "2026-05-21T14:34:37.332Z" },
+ { url = "https://files.pythonhosted.org/packages/45/91/254a35c20acc38a7223c9d2d594af12e794432464f2cdeb52af1dc4a892d/ruff-0.15.14-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:be4ff55af755bd71a00ab3dc6bd7ffc467bd76e0df6881e286c2e3d23e8fb43b", size = 11144969, upload-time = "2026-05-21T14:34:43.978Z" },
+ { url = "https://files.pythonhosted.org/packages/56/9e/d13e40f83b8d0a94430e6778ce1d94a43b38cf2efe63278bdd2b4c65abbf/ruff-0.15.14-py3-none-macosx_11_0_arm64.whl", hash = "sha256:48d5909d7d06276ce7dde6d32bfa4b0d4cb2651145cd8ee4b440722cbc77832f", size = 10478207, upload-time = "2026-05-21T14:34:48.378Z" },
+ { url = "https://files.pythonhosted.org/packages/8d/f1/b15a7839fa4f332f8acec78e20564f26bb2d866e3d21710b877fd0263000/ruff-0.15.14-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca8cbfa94c4f90984a67561978602746d4cd27103568f745fa90eee3f0d4107d", size = 10818459, upload-time = "2026-05-21T14:34:22.318Z" },
+ { url = "https://files.pythonhosted.org/packages/45/33/53d651177f84f94b400a0e27f8824eeada3dddc9d5ee8aeb048f4352a520/ruff-0.15.14-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9a6bbc0333f1ab053423bcbf6226477d266ca7cec7738c4c8e3f55647803f3c4", size = 10541800, upload-time = "2026-05-21T14:34:20.209Z" },
+ { url = "https://files.pythonhosted.org/packages/b8/a6/868f87e0bf9786ed24b5d0d0ad8676b8a94fd1912f42cddf9cfc7857818a/ruff-0.15.14-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8a24a4f7605d7003a6674d4387651effd939dead3fddd0f36561eb77a9a2e542", size = 11342149, upload-time = "2026-05-21T14:34:46.365Z" },
+ { url = "https://files.pythonhosted.org/packages/a7/8b/38cd5c19faffdcc05a408d2b78edccc69492ab9720eadb49ea15ef80d768/ruff-0.15.14-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:049b5326e53ed80978f2fc041a280603f69dd6b0c95464342a2bb4572d9d9e2f", size = 12212563, upload-time = "2026-05-21T14:34:28.579Z" },
+ { url = "https://files.pythonhosted.org/packages/3e/4d/a3c5b874a556d5731e3e657aaf04311bb76f0a5c3ec220ed43051be6b64b/ruff-0.15.14-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d4ed42e6696c8dfa5f06728e6441993901f548eb92d73bc472cb5a38d1395fbf", size = 11493299, upload-time = "2026-05-21T14:34:41.836Z" },
+ { url = "https://files.pythonhosted.org/packages/1e/c0/56472c251d09858a53e51efbd485b09e1995d8731668b76d52e5dd6ee0f1/ruff-0.15.14-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:715c543cf450c4888251f91c52f1942a800541d9bddd7ac060aa4e6b77ae7cba", size = 11455931, upload-time = "2026-05-21T14:34:57.276Z" },
+ { url = "https://files.pythonhosted.org/packages/2c/4a/e2e7b4d8dbf233d4eace59c75bc3435fa6d8bd3bae82d351d4e4300c0fd1/ruff-0.15.14-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:72ebab6013ec887d439d8b7593737a0a4ffb06d45d209d4e4bf2e92813082d3f", size = 11400794, upload-time = "2026-05-21T14:34:39.773Z" },
+ { url = "https://files.pythonhosted.org/packages/97/c7/83c0539fe34c3e09136204d1e75d6052492364e0b3cb05e9465423f567d7/ruff-0.15.14-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:49072d36abdbe97a8dd7f480afe9c675699c0c495d4c84076e2c1203c4550581", size = 10804759, upload-time = "2026-05-21T14:34:31.045Z" },
+ { url = "https://files.pythonhosted.org/packages/86/a6/18f2bfc095a2ab4a78745644e428205532ce6653a5d0fa8501572891534d/ruff-0.15.14-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:958522aee105068640c2c2ceae08f413ae44d922f52a1374ac13d6a96032fc93", size = 10539517, upload-time = "2026-05-21T14:34:53.064Z" },
+ { url = "https://files.pythonhosted.org/packages/54/3a/5a8b3b69c654d4e4bf1d246ac5b49cbcdac6eaab6905925f8915f31e3b80/ruff-0.15.14-py3-none-musllinux_1_2_i686.whl", hash = "sha256:f3707da619a143a2e8830e2abab8224478d69ace2d28cb6c20543ae97c36bf61", size = 11065169, upload-time = "2026-05-21T14:34:24.484Z" },
+ { url = "https://files.pythonhosted.org/packages/ed/c5/8864e4e7925b836ea354b31d57641ec03830564e281a8b6f061f8c3e0ec1/ruff-0.15.14-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:bb01d645694e3ec0102105d07ef2d53703970407d59c04e59d3ba0b7a1d53553", size = 11560214, upload-time = "2026-05-21T14:34:50.975Z" },
+ { url = "https://files.pythonhosted.org/packages/36/38/012bf76752e1f89ed50b77b99532d90f3a3e287bc7918e1fc0948ac866ac/ruff-0.15.14-py3-none-win32.whl", hash = "sha256:6d0c1ad2a0ab718d39b6d8fd2217981ce4d625cd96a720095f798fb47d8b13e6", size = 10805548, upload-time = "2026-05-21T14:34:33.453Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/b7/4ea2c170f10ad760fff2a5250beb18897719dc8b52b53a24cddbb9dd3f19/ruff-0.15.14-py3-none-win_amd64.whl", hash = "sha256:802342981e056db3851a7836e5b070f8f15f67d4a685ae2a6160939d364b2902", size = 11939523, upload-time = "2026-05-21T14:34:18.077Z" },
+ { url = "https://files.pythonhosted.org/packages/62/d5/bc97ff895ec35cf3925d4bd60f3b39d822f377a446906ec9bcc87405e59b/ruff-0.15.14-py3-none-win_arm64.whl", hash = "sha256:ff47b90a9ef6a40c9e2f3b479c1fb78531adf055b94c1eba0a7ba04b31951826", size = 11208607, upload-time = "2026-05-21T14:34:26.525Z" },
]
[[package]]
@@ -1245,20 +1246,20 @@ version = "0.7.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/29/9c/6e74567782559a63bd040a236edca26fd71bc7ba88de2ef35d75df3bca5e/safetensors-0.7.0.tar.gz", hash = "sha256:07663963b67e8bd9f0b8ad15bb9163606cd27cc5a1b96235a50d8369803b96b0", size = 200878, upload-time = "2025-11-19T15:18:43.199Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/fa/47/aef6c06649039accf914afef490268e1067ed82be62bcfa5b7e886ad15e8/safetensors-0.7.0-cp38-abi3-macosx_10_12_x86_64.whl", hash = "sha256:c82f4d474cf725255d9e6acf17252991c3c8aac038d6ef363a4bf8be2f6db517", size = 467781, upload-time = "2025-11-19T15:18:35.84Z" },
- { url = "https://files.pythonhosted.org/packages/e8/00/374c0c068e30cd31f1e1b46b4b5738168ec79e7689ca82ee93ddfea05109/safetensors-0.7.0-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:94fd4858284736bb67a897a41608b5b0c2496c9bdb3bf2af1fa3409127f20d57", size = 447058, upload-time = "2025-11-19T15:18:34.416Z" },
- { url = "https://files.pythonhosted.org/packages/f1/06/578ffed52c2296f93d7fd2d844cabfa92be51a587c38c8afbb8ae449ca89/safetensors-0.7.0-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e07d91d0c92a31200f25351f4acb2bc6aff7f48094e13ebb1d0fb995b54b6542", size = 491748, upload-time = "2025-11-19T15:18:09.79Z" },
- { url = "https://files.pythonhosted.org/packages/ae/33/1debbbb70e4791dde185edb9413d1fe01619255abb64b300157d7f15dddd/safetensors-0.7.0-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8469155f4cb518bafb4acf4865e8bb9d6804110d2d9bdcaa78564b9fd841e104", size = 503881, upload-time = "2025-11-19T15:18:16.145Z" },
- { url = "https://files.pythonhosted.org/packages/8e/1c/40c2ca924d60792c3be509833df711b553c60effbd91da6f5284a83f7122/safetensors-0.7.0-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:54bef08bf00a2bff599982f6b08e8770e09cc012d7bba00783fc7ea38f1fb37d", size = 623463, upload-time = "2025-11-19T15:18:21.11Z" },
- { url = "https://files.pythonhosted.org/packages/9b/3a/13784a9364bd43b0d61eef4bea2845039bc2030458b16594a1bd787ae26e/safetensors-0.7.0-cp38-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:42cb091236206bb2016d245c377ed383aa7f78691748f3bb6ee1bfa51ae2ce6a", size = 532855, upload-time = "2025-11-19T15:18:25.719Z" },
- { url = "https://files.pythonhosted.org/packages/a0/60/429e9b1cb3fc651937727befe258ea24122d9663e4d5709a48c9cbfceecb/safetensors-0.7.0-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dac7252938f0696ddea46f5e855dd3138444e82236e3be475f54929f0c510d48", size = 507152, upload-time = "2025-11-19T15:18:33.023Z" },
- { url = "https://files.pythonhosted.org/packages/3c/a8/4b45e4e059270d17af60359713ffd83f97900d45a6afa73aaa0d737d48b6/safetensors-0.7.0-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1d060c70284127fa805085d8f10fbd0962792aed71879d00864acda69dbab981", size = 541856, upload-time = "2025-11-19T15:18:31.075Z" },
- { url = "https://files.pythonhosted.org/packages/06/87/d26d8407c44175d8ae164a95b5a62707fcc445f3c0c56108e37d98070a3d/safetensors-0.7.0-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:cdab83a366799fa730f90a4ebb563e494f28e9e92c4819e556152ad55e43591b", size = 674060, upload-time = "2025-11-19T15:18:37.211Z" },
- { url = "https://files.pythonhosted.org/packages/11/f5/57644a2ff08dc6325816ba7217e5095f17269dada2554b658442c66aed51/safetensors-0.7.0-cp38-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:672132907fcad9f2aedcb705b2d7b3b93354a2aec1b2f706c4db852abe338f85", size = 771715, upload-time = "2025-11-19T15:18:38.689Z" },
- { url = "https://files.pythonhosted.org/packages/86/31/17883e13a814bd278ae6e266b13282a01049b0c81341da7fd0e3e71a80a3/safetensors-0.7.0-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:5d72abdb8a4d56d4020713724ba81dac065fedb7f3667151c4a637f1d3fb26c0", size = 714377, upload-time = "2025-11-19T15:18:40.162Z" },
- { url = "https://files.pythonhosted.org/packages/4a/d8/0c8a7dc9b41dcac53c4cbf9df2b9c83e0e0097203de8b37a712b345c0be5/safetensors-0.7.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b0f6d66c1c538d5a94a73aa9ddca8ccc4227e6c9ff555322ea40bdd142391dd4", size = 677368, upload-time = "2025-11-19T15:18:41.627Z" },
- { url = "https://files.pythonhosted.org/packages/05/e5/cb4b713c8a93469e3c5be7c3f8d77d307e65fe89673e731f5c2bfd0a9237/safetensors-0.7.0-cp38-abi3-win32.whl", hash = "sha256:c74af94bf3ac15ac4d0f2a7c7b4663a15f8c2ab15ed0fc7531ca61d0835eccba", size = 326423, upload-time = "2025-11-19T15:18:45.74Z" },
- { url = "https://files.pythonhosted.org/packages/5d/e6/ec8471c8072382cb91233ba7267fd931219753bb43814cbc71757bfd4dab/safetensors-0.7.0-cp38-abi3-win_amd64.whl", hash = "sha256:d1239932053f56f3456f32eb9625590cc7582e905021f94636202a864d470755", size = 341380, upload-time = "2025-11-19T15:18:44.427Z" },
+ { url = "https://files.pythonhosted.org/packages/fa/47/aef6c06649039accf914afef490268e1067ed82be62bcfa5b7e886ad15e8/safetensors-0.7.0-cp38-abi3-macosx_10_12_x86_64.whl", hash = "sha256:c82f4d474cf725255d9e6acf17252991c3c8aac038d6ef363a4bf8be2f6db517", size = 467781, upload-time = "2025-11-19T15:18:35.84Z" },
+ { url = "https://files.pythonhosted.org/packages/e8/00/374c0c068e30cd31f1e1b46b4b5738168ec79e7689ca82ee93ddfea05109/safetensors-0.7.0-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:94fd4858284736bb67a897a41608b5b0c2496c9bdb3bf2af1fa3409127f20d57", size = 447058, upload-time = "2025-11-19T15:18:34.416Z" },
+ { url = "https://files.pythonhosted.org/packages/f1/06/578ffed52c2296f93d7fd2d844cabfa92be51a587c38c8afbb8ae449ca89/safetensors-0.7.0-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e07d91d0c92a31200f25351f4acb2bc6aff7f48094e13ebb1d0fb995b54b6542", size = 491748, upload-time = "2025-11-19T15:18:09.79Z" },
+ { url = "https://files.pythonhosted.org/packages/ae/33/1debbbb70e4791dde185edb9413d1fe01619255abb64b300157d7f15dddd/safetensors-0.7.0-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8469155f4cb518bafb4acf4865e8bb9d6804110d2d9bdcaa78564b9fd841e104", size = 503881, upload-time = "2025-11-19T15:18:16.145Z" },
+ { url = "https://files.pythonhosted.org/packages/8e/1c/40c2ca924d60792c3be509833df711b553c60effbd91da6f5284a83f7122/safetensors-0.7.0-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:54bef08bf00a2bff599982f6b08e8770e09cc012d7bba00783fc7ea38f1fb37d", size = 623463, upload-time = "2025-11-19T15:18:21.11Z" },
+ { url = "https://files.pythonhosted.org/packages/9b/3a/13784a9364bd43b0d61eef4bea2845039bc2030458b16594a1bd787ae26e/safetensors-0.7.0-cp38-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:42cb091236206bb2016d245c377ed383aa7f78691748f3bb6ee1bfa51ae2ce6a", size = 532855, upload-time = "2025-11-19T15:18:25.719Z" },
+ { url = "https://files.pythonhosted.org/packages/a0/60/429e9b1cb3fc651937727befe258ea24122d9663e4d5709a48c9cbfceecb/safetensors-0.7.0-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dac7252938f0696ddea46f5e855dd3138444e82236e3be475f54929f0c510d48", size = 507152, upload-time = "2025-11-19T15:18:33.023Z" },
+ { url = "https://files.pythonhosted.org/packages/3c/a8/4b45e4e059270d17af60359713ffd83f97900d45a6afa73aaa0d737d48b6/safetensors-0.7.0-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1d060c70284127fa805085d8f10fbd0962792aed71879d00864acda69dbab981", size = 541856, upload-time = "2025-11-19T15:18:31.075Z" },
+ { url = "https://files.pythonhosted.org/packages/06/87/d26d8407c44175d8ae164a95b5a62707fcc445f3c0c56108e37d98070a3d/safetensors-0.7.0-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:cdab83a366799fa730f90a4ebb563e494f28e9e92c4819e556152ad55e43591b", size = 674060, upload-time = "2025-11-19T15:18:37.211Z" },
+ { url = "https://files.pythonhosted.org/packages/11/f5/57644a2ff08dc6325816ba7217e5095f17269dada2554b658442c66aed51/safetensors-0.7.0-cp38-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:672132907fcad9f2aedcb705b2d7b3b93354a2aec1b2f706c4db852abe338f85", size = 771715, upload-time = "2025-11-19T15:18:38.689Z" },
+ { url = "https://files.pythonhosted.org/packages/86/31/17883e13a814bd278ae6e266b13282a01049b0c81341da7fd0e3e71a80a3/safetensors-0.7.0-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:5d72abdb8a4d56d4020713724ba81dac065fedb7f3667151c4a637f1d3fb26c0", size = 714377, upload-time = "2025-11-19T15:18:40.162Z" },
+ { url = "https://files.pythonhosted.org/packages/4a/d8/0c8a7dc9b41dcac53c4cbf9df2b9c83e0e0097203de8b37a712b345c0be5/safetensors-0.7.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b0f6d66c1c538d5a94a73aa9ddca8ccc4227e6c9ff555322ea40bdd142391dd4", size = 677368, upload-time = "2025-11-19T15:18:41.627Z" },
+ { url = "https://files.pythonhosted.org/packages/05/e5/cb4b713c8a93469e3c5be7c3f8d77d307e65fe89673e731f5c2bfd0a9237/safetensors-0.7.0-cp38-abi3-win32.whl", hash = "sha256:c74af94bf3ac15ac4d0f2a7c7b4663a15f8c2ab15ed0fc7531ca61d0835eccba", size = 326423, upload-time = "2025-11-19T15:18:45.74Z" },
+ { url = "https://files.pythonhosted.org/packages/5d/e6/ec8471c8072382cb91233ba7267fd931219753bb43814cbc71757bfd4dab/safetensors-0.7.0-cp38-abi3-win_amd64.whl", hash = "sha256:d1239932053f56f3456f32eb9625590cc7582e905021f94636202a864d470755", size = 341380, upload-time = "2025-11-19T15:18:44.427Z" },
]
[[package]]
@@ -1266,37 +1267,37 @@ name = "scikit-learn"
version = "1.8.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
- { name = "joblib" },
- { name = "numpy" },
- { name = "scipy" },
- { name = "threadpoolctl" },
+ { name = "joblib" },
+ { name = "numpy" },
+ { name = "scipy" },
+ { name = "threadpoolctl" },
]
sdist = { url = "https://files.pythonhosted.org/packages/0e/d4/40988bf3b8e34feec1d0e6a051446b1f66225f8529b9309becaeef62b6c4/scikit_learn-1.8.0.tar.gz", hash = "sha256:9bccbb3b40e3de10351f8f5068e105d0f4083b1a65fa07b6634fbc401a6287fd", size = 7335585, upload-time = "2025-12-10T07:08:53.618Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/03/aa/e22e0768512ce9255eba34775be2e85c2048da73da1193e841707f8f039c/scikit_learn-1.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0d6ae97234d5d7079dc0040990a6f7aeb97cb7fa7e8945f1999a429b23569e0a", size = 8513770, upload-time = "2025-12-10T07:08:03.251Z" },
- { url = "https://files.pythonhosted.org/packages/58/37/31b83b2594105f61a381fc74ca19e8780ee923be2d496fcd8d2e1147bd99/scikit_learn-1.8.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:edec98c5e7c128328124a029bceb09eda2d526997780fef8d65e9a69eead963e", size = 8044458, upload-time = "2025-12-10T07:08:05.336Z" },
- { url = "https://files.pythonhosted.org/packages/2d/5a/3f1caed8765f33eabb723596666da4ebbf43d11e96550fb18bdec42b467b/scikit_learn-1.8.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:74b66d8689d52ed04c271e1329f0c61635bcaf5b926db9b12d58914cdc01fe57", size = 8610341, upload-time = "2025-12-10T07:08:07.732Z" },
- { url = "https://files.pythonhosted.org/packages/38/cf/06896db3f71c75902a8e9943b444a56e727418f6b4b4a90c98c934f51ed4/scikit_learn-1.8.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8fdf95767f989b0cfedb85f7ed8ca215d4be728031f56ff5a519ee1e3276dc2e", size = 8900022, upload-time = "2025-12-10T07:08:09.862Z" },
- { url = "https://files.pythonhosted.org/packages/1c/f9/9b7563caf3ec8873e17a31401858efab6b39a882daf6c1bfa88879c0aa11/scikit_learn-1.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:2de443b9373b3b615aec1bb57f9baa6bb3a9bd093f1269ba95c17d870422b271", size = 7989409, upload-time = "2025-12-10T07:08:12.028Z" },
- { url = "https://files.pythonhosted.org/packages/49/bd/1f4001503650e72c4f6009ac0c4413cb17d2d601cef6f71c0453da2732fc/scikit_learn-1.8.0-cp313-cp313-win_arm64.whl", hash = "sha256:eddde82a035681427cbedded4e6eff5e57fa59216c2e3e90b10b19ab1d0a65c3", size = 7619760, upload-time = "2025-12-10T07:08:13.688Z" },
- { url = "https://files.pythonhosted.org/packages/d2/7d/a630359fc9dcc95496588c8d8e3245cc8fd81980251079bc09c70d41d951/scikit_learn-1.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:7cc267b6108f0a1499a734167282c00c4ebf61328566b55ef262d48e9849c735", size = 8826045, upload-time = "2025-12-10T07:08:15.215Z" },
- { url = "https://files.pythonhosted.org/packages/cc/56/a0c86f6930cfcd1c7054a2bc417e26960bb88d32444fe7f71d5c2cfae891/scikit_learn-1.8.0-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:fe1c011a640a9f0791146011dfd3c7d9669785f9fed2b2a5f9e207536cf5c2fd", size = 8420324, upload-time = "2025-12-10T07:08:17.561Z" },
- { url = "https://files.pythonhosted.org/packages/46/1e/05962ea1cebc1cf3876667ecb14c283ef755bf409993c5946ade3b77e303/scikit_learn-1.8.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:72358cce49465d140cc4e7792015bb1f0296a9742d5622c67e31399b75468b9e", size = 8680651, upload-time = "2025-12-10T07:08:19.952Z" },
- { url = "https://files.pythonhosted.org/packages/fe/56/a85473cd75f200c9759e3a5f0bcab2d116c92a8a02ee08ccd73b870f8bb4/scikit_learn-1.8.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:80832434a6cc114f5219211eec13dcbc16c2bac0e31ef64c6d346cde3cf054cb", size = 8925045, upload-time = "2025-12-10T07:08:22.11Z" },
- { url = "https://files.pythonhosted.org/packages/cc/b7/64d8cfa896c64435ae57f4917a548d7ac7a44762ff9802f75a79b77cb633/scikit_learn-1.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ee787491dbfe082d9c3013f01f5991658b0f38aa8177e4cd4bf434c58f551702", size = 8507994, upload-time = "2025-12-10T07:08:23.943Z" },
- { url = "https://files.pythonhosted.org/packages/5e/37/e192ea709551799379958b4c4771ec507347027bb7c942662c7fbeba31cb/scikit_learn-1.8.0-cp313-cp313t-win_arm64.whl", hash = "sha256:bf97c10a3f5a7543f9b88cbf488d33d175e9146115a451ae34568597ba33dcde", size = 7869518, upload-time = "2025-12-10T07:08:25.71Z" },
- { url = "https://files.pythonhosted.org/packages/24/05/1af2c186174cc92dcab2233f327336058c077d38f6fe2aceb08e6ab4d509/scikit_learn-1.8.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:c22a2da7a198c28dd1a6e1136f19c830beab7fdca5b3e5c8bba8394f8a5c45b3", size = 8528667, upload-time = "2025-12-10T07:08:27.541Z" },
- { url = "https://files.pythonhosted.org/packages/a8/25/01c0af38fe969473fb292bba9dc2b8f9b451f3112ff242c647fee3d0dfe7/scikit_learn-1.8.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:6b595b07a03069a2b1740dc08c2299993850ea81cce4fe19b2421e0c970de6b7", size = 8066524, upload-time = "2025-12-10T07:08:29.822Z" },
- { url = "https://files.pythonhosted.org/packages/be/ce/a0623350aa0b68647333940ee46fe45086c6060ec604874e38e9ab7d8e6c/scikit_learn-1.8.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:29ffc74089f3d5e87dfca4c2c8450f88bdc61b0fc6ed5d267f3988f19a1309f6", size = 8657133, upload-time = "2025-12-10T07:08:31.865Z" },
- { url = "https://files.pythonhosted.org/packages/b8/cb/861b41341d6f1245e6ca80b1c1a8c4dfce43255b03df034429089ca2a2c5/scikit_learn-1.8.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fb65db5d7531bccf3a4f6bec3462223bea71384e2cda41da0f10b7c292b9e7c4", size = 8923223, upload-time = "2025-12-10T07:08:34.166Z" },
- { url = "https://files.pythonhosted.org/packages/76/18/a8def8f91b18cd1ba6e05dbe02540168cb24d47e8dcf69e8d00b7da42a08/scikit_learn-1.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:56079a99c20d230e873ea40753102102734c5953366972a71d5cb39a32bc40c6", size = 8096518, upload-time = "2025-12-10T07:08:36.339Z" },
- { url = "https://files.pythonhosted.org/packages/d1/77/482076a678458307f0deb44e29891d6022617b2a64c840c725495bee343f/scikit_learn-1.8.0-cp314-cp314-win_arm64.whl", hash = "sha256:3bad7565bc9cf37ce19a7c0d107742b320c1285df7aab1a6e2d28780df167242", size = 7754546, upload-time = "2025-12-10T07:08:38.128Z" },
- { url = "https://files.pythonhosted.org/packages/2d/d1/ef294ca754826daa043b2a104e59960abfab4cf653891037d19dd5b6f3cf/scikit_learn-1.8.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:4511be56637e46c25721e83d1a9cea9614e7badc7040c4d573d75fbe257d6fd7", size = 8848305, upload-time = "2025-12-10T07:08:41.013Z" },
- { url = "https://files.pythonhosted.org/packages/5b/e2/b1f8b05138ee813b8e1a4149f2f0d289547e60851fd1bb268886915adbda/scikit_learn-1.8.0-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:a69525355a641bf8ef136a7fa447672fb54fe8d60cab5538d9eb7c6438543fb9", size = 8432257, upload-time = "2025-12-10T07:08:42.873Z" },
- { url = "https://files.pythonhosted.org/packages/26/11/c32b2138a85dcb0c99f6afd13a70a951bfdff8a6ab42d8160522542fb647/scikit_learn-1.8.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c2656924ec73e5939c76ac4c8b026fc203b83d8900362eb2599d8aee80e4880f", size = 8678673, upload-time = "2025-12-10T07:08:45.362Z" },
- { url = "https://files.pythonhosted.org/packages/c7/57/51f2384575bdec454f4fe4e7a919d696c9ebce914590abf3e52d47607ab8/scikit_learn-1.8.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15fc3b5d19cc2be65404786857f2e13c70c83dd4782676dd6814e3b89dc8f5b9", size = 8922467, upload-time = "2025-12-10T07:08:47.408Z" },
- { url = "https://files.pythonhosted.org/packages/35/4d/748c9e2872637a57981a04adc038dacaa16ba8ca887b23e34953f0b3f742/scikit_learn-1.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:00d6f1d66fbcf4eba6e356e1420d33cc06c70a45bb1363cd6f6a8e4ebbbdece2", size = 8774395, upload-time = "2025-12-10T07:08:49.337Z" },
- { url = "https://files.pythonhosted.org/packages/60/22/d7b2ebe4704a5e50790ba089d5c2ae308ab6bb852719e6c3bd4f04c3a363/scikit_learn-1.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:f28dd15c6bb0b66ba09728cf09fd8736c304be29409bd8445a080c1280619e8c", size = 8002647, upload-time = "2025-12-10T07:08:51.601Z" },
+ { url = "https://files.pythonhosted.org/packages/03/aa/e22e0768512ce9255eba34775be2e85c2048da73da1193e841707f8f039c/scikit_learn-1.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0d6ae97234d5d7079dc0040990a6f7aeb97cb7fa7e8945f1999a429b23569e0a", size = 8513770, upload-time = "2025-12-10T07:08:03.251Z" },
+ { url = "https://files.pythonhosted.org/packages/58/37/31b83b2594105f61a381fc74ca19e8780ee923be2d496fcd8d2e1147bd99/scikit_learn-1.8.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:edec98c5e7c128328124a029bceb09eda2d526997780fef8d65e9a69eead963e", size = 8044458, upload-time = "2025-12-10T07:08:05.336Z" },
+ { url = "https://files.pythonhosted.org/packages/2d/5a/3f1caed8765f33eabb723596666da4ebbf43d11e96550fb18bdec42b467b/scikit_learn-1.8.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:74b66d8689d52ed04c271e1329f0c61635bcaf5b926db9b12d58914cdc01fe57", size = 8610341, upload-time = "2025-12-10T07:08:07.732Z" },
+ { url = "https://files.pythonhosted.org/packages/38/cf/06896db3f71c75902a8e9943b444a56e727418f6b4b4a90c98c934f51ed4/scikit_learn-1.8.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8fdf95767f989b0cfedb85f7ed8ca215d4be728031f56ff5a519ee1e3276dc2e", size = 8900022, upload-time = "2025-12-10T07:08:09.862Z" },
+ { url = "https://files.pythonhosted.org/packages/1c/f9/9b7563caf3ec8873e17a31401858efab6b39a882daf6c1bfa88879c0aa11/scikit_learn-1.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:2de443b9373b3b615aec1bb57f9baa6bb3a9bd093f1269ba95c17d870422b271", size = 7989409, upload-time = "2025-12-10T07:08:12.028Z" },
+ { url = "https://files.pythonhosted.org/packages/49/bd/1f4001503650e72c4f6009ac0c4413cb17d2d601cef6f71c0453da2732fc/scikit_learn-1.8.0-cp313-cp313-win_arm64.whl", hash = "sha256:eddde82a035681427cbedded4e6eff5e57fa59216c2e3e90b10b19ab1d0a65c3", size = 7619760, upload-time = "2025-12-10T07:08:13.688Z" },
+ { url = "https://files.pythonhosted.org/packages/d2/7d/a630359fc9dcc95496588c8d8e3245cc8fd81980251079bc09c70d41d951/scikit_learn-1.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:7cc267b6108f0a1499a734167282c00c4ebf61328566b55ef262d48e9849c735", size = 8826045, upload-time = "2025-12-10T07:08:15.215Z" },
+ { url = "https://files.pythonhosted.org/packages/cc/56/a0c86f6930cfcd1c7054a2bc417e26960bb88d32444fe7f71d5c2cfae891/scikit_learn-1.8.0-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:fe1c011a640a9f0791146011dfd3c7d9669785f9fed2b2a5f9e207536cf5c2fd", size = 8420324, upload-time = "2025-12-10T07:08:17.561Z" },
+ { url = "https://files.pythonhosted.org/packages/46/1e/05962ea1cebc1cf3876667ecb14c283ef755bf409993c5946ade3b77e303/scikit_learn-1.8.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:72358cce49465d140cc4e7792015bb1f0296a9742d5622c67e31399b75468b9e", size = 8680651, upload-time = "2025-12-10T07:08:19.952Z" },
+ { url = "https://files.pythonhosted.org/packages/fe/56/a85473cd75f200c9759e3a5f0bcab2d116c92a8a02ee08ccd73b870f8bb4/scikit_learn-1.8.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:80832434a6cc114f5219211eec13dcbc16c2bac0e31ef64c6d346cde3cf054cb", size = 8925045, upload-time = "2025-12-10T07:08:22.11Z" },
+ { url = "https://files.pythonhosted.org/packages/cc/b7/64d8cfa896c64435ae57f4917a548d7ac7a44762ff9802f75a79b77cb633/scikit_learn-1.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ee787491dbfe082d9c3013f01f5991658b0f38aa8177e4cd4bf434c58f551702", size = 8507994, upload-time = "2025-12-10T07:08:23.943Z" },
+ { url = "https://files.pythonhosted.org/packages/5e/37/e192ea709551799379958b4c4771ec507347027bb7c942662c7fbeba31cb/scikit_learn-1.8.0-cp313-cp313t-win_arm64.whl", hash = "sha256:bf97c10a3f5a7543f9b88cbf488d33d175e9146115a451ae34568597ba33dcde", size = 7869518, upload-time = "2025-12-10T07:08:25.71Z" },
+ { url = "https://files.pythonhosted.org/packages/24/05/1af2c186174cc92dcab2233f327336058c077d38f6fe2aceb08e6ab4d509/scikit_learn-1.8.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:c22a2da7a198c28dd1a6e1136f19c830beab7fdca5b3e5c8bba8394f8a5c45b3", size = 8528667, upload-time = "2025-12-10T07:08:27.541Z" },
+ { url = "https://files.pythonhosted.org/packages/a8/25/01c0af38fe969473fb292bba9dc2b8f9b451f3112ff242c647fee3d0dfe7/scikit_learn-1.8.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:6b595b07a03069a2b1740dc08c2299993850ea81cce4fe19b2421e0c970de6b7", size = 8066524, upload-time = "2025-12-10T07:08:29.822Z" },
+ { url = "https://files.pythonhosted.org/packages/be/ce/a0623350aa0b68647333940ee46fe45086c6060ec604874e38e9ab7d8e6c/scikit_learn-1.8.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:29ffc74089f3d5e87dfca4c2c8450f88bdc61b0fc6ed5d267f3988f19a1309f6", size = 8657133, upload-time = "2025-12-10T07:08:31.865Z" },
+ { url = "https://files.pythonhosted.org/packages/b8/cb/861b41341d6f1245e6ca80b1c1a8c4dfce43255b03df034429089ca2a2c5/scikit_learn-1.8.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fb65db5d7531bccf3a4f6bec3462223bea71384e2cda41da0f10b7c292b9e7c4", size = 8923223, upload-time = "2025-12-10T07:08:34.166Z" },
+ { url = "https://files.pythonhosted.org/packages/76/18/a8def8f91b18cd1ba6e05dbe02540168cb24d47e8dcf69e8d00b7da42a08/scikit_learn-1.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:56079a99c20d230e873ea40753102102734c5953366972a71d5cb39a32bc40c6", size = 8096518, upload-time = "2025-12-10T07:08:36.339Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/77/482076a678458307f0deb44e29891d6022617b2a64c840c725495bee343f/scikit_learn-1.8.0-cp314-cp314-win_arm64.whl", hash = "sha256:3bad7565bc9cf37ce19a7c0d107742b320c1285df7aab1a6e2d28780df167242", size = 7754546, upload-time = "2025-12-10T07:08:38.128Z" },
+ { url = "https://files.pythonhosted.org/packages/2d/d1/ef294ca754826daa043b2a104e59960abfab4cf653891037d19dd5b6f3cf/scikit_learn-1.8.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:4511be56637e46c25721e83d1a9cea9614e7badc7040c4d573d75fbe257d6fd7", size = 8848305, upload-time = "2025-12-10T07:08:41.013Z" },
+ { url = "https://files.pythonhosted.org/packages/5b/e2/b1f8b05138ee813b8e1a4149f2f0d289547e60851fd1bb268886915adbda/scikit_learn-1.8.0-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:a69525355a641bf8ef136a7fa447672fb54fe8d60cab5538d9eb7c6438543fb9", size = 8432257, upload-time = "2025-12-10T07:08:42.873Z" },
+ { url = "https://files.pythonhosted.org/packages/26/11/c32b2138a85dcb0c99f6afd13a70a951bfdff8a6ab42d8160522542fb647/scikit_learn-1.8.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c2656924ec73e5939c76ac4c8b026fc203b83d8900362eb2599d8aee80e4880f", size = 8678673, upload-time = "2025-12-10T07:08:45.362Z" },
+ { url = "https://files.pythonhosted.org/packages/c7/57/51f2384575bdec454f4fe4e7a919d696c9ebce914590abf3e52d47607ab8/scikit_learn-1.8.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15fc3b5d19cc2be65404786857f2e13c70c83dd4782676dd6814e3b89dc8f5b9", size = 8922467, upload-time = "2025-12-10T07:08:47.408Z" },
+ { url = "https://files.pythonhosted.org/packages/35/4d/748c9e2872637a57981a04adc038dacaa16ba8ca887b23e34953f0b3f742/scikit_learn-1.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:00d6f1d66fbcf4eba6e356e1420d33cc06c70a45bb1363cd6f6a8e4ebbbdece2", size = 8774395, upload-time = "2025-12-10T07:08:49.337Z" },
+ { url = "https://files.pythonhosted.org/packages/60/22/d7b2ebe4704a5e50790ba089d5c2ae308ab6bb852719e6c3bd4f04c3a363/scikit_learn-1.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:f28dd15c6bb0b66ba09728cf09fd8736c304be29409bd8445a080c1280619e8c", size = 8002647, upload-time = "2025-12-10T07:08:51.601Z" },
]
[[package]]
@@ -1304,50 +1305,50 @@ name = "scipy"
version = "1.17.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
- { name = "numpy" },
+ { name = "numpy" },
]
sdist = { url = "https://files.pythonhosted.org/packages/7a/97/5a3609c4f8d58b039179648e62dd220f89864f56f7357f5d4f45c29eb2cc/scipy-1.17.1.tar.gz", hash = "sha256:95d8e012d8cb8816c226aef832200b1d45109ed4464303e997c5b13122b297c0", size = 30573822, upload-time = "2026-02-23T00:26:24.851Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/76/27/07ee1b57b65e92645f219b37148a7e7928b82e2b5dbeccecb4dff7c64f0b/scipy-1.17.1-cp313-cp313-macosx_10_14_x86_64.whl", hash = "sha256:5e3c5c011904115f88a39308379c17f91546f77c1667cea98739fe0fccea804c", size = 31590199, upload-time = "2026-02-23T00:19:17.192Z" },
- { url = "https://files.pythonhosted.org/packages/ec/ae/db19f8ab842e9b724bf5dbb7db29302a91f1e55bc4d04b1025d6d605a2c5/scipy-1.17.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:6fac755ca3d2c3edcb22f479fceaa241704111414831ddd3bc6056e18516892f", size = 28154001, upload-time = "2026-02-23T00:19:22.241Z" },
- { url = "https://files.pythonhosted.org/packages/5b/58/3ce96251560107b381cbd6e8413c483bbb1228a6b919fa8652b0d4090e7f/scipy-1.17.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:7ff200bf9d24f2e4d5dc6ee8c3ac64d739d3a89e2326ba68aaf6c4a2b838fd7d", size = 20325719, upload-time = "2026-02-23T00:19:26.329Z" },
- { url = "https://files.pythonhosted.org/packages/b2/83/15087d945e0e4d48ce2377498abf5ad171ae013232ae31d06f336e64c999/scipy-1.17.1-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:4b400bdc6f79fa02a4d86640310dde87a21fba0c979efff5248908c6f15fad1b", size = 22683595, upload-time = "2026-02-23T00:19:30.304Z" },
- { url = "https://files.pythonhosted.org/packages/b4/e0/e58fbde4a1a594c8be8114eb4aac1a55bcd6587047efc18a61eb1f5c0d30/scipy-1.17.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2b64ca7d4aee0102a97f3ba22124052b4bd2152522355073580bf4845e2550b6", size = 32896429, upload-time = "2026-02-23T00:19:35.536Z" },
- { url = "https://files.pythonhosted.org/packages/f5/5f/f17563f28ff03c7b6799c50d01d5d856a1d55f2676f537ca8d28c7f627cd/scipy-1.17.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:581b2264fc0aa555f3f435a5944da7504ea3a065d7029ad60e7c3d1ae09c5464", size = 35203952, upload-time = "2026-02-23T00:19:42.259Z" },
- { url = "https://files.pythonhosted.org/packages/8d/a5/9afd17de24f657fdfe4df9a3f1ea049b39aef7c06000c13db1530d81ccca/scipy-1.17.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:beeda3d4ae615106d7094f7e7cef6218392e4465cc95d25f900bebabfded0950", size = 34979063, upload-time = "2026-02-23T00:19:47.547Z" },
- { url = "https://files.pythonhosted.org/packages/8b/13/88b1d2384b424bf7c924f2038c1c409f8d88bb2a8d49d097861dd64a57b2/scipy-1.17.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6609bc224e9568f65064cfa72edc0f24ee6655b47575954ec6339534b2798369", size = 37598449, upload-time = "2026-02-23T00:19:53.238Z" },
- { url = "https://files.pythonhosted.org/packages/35/e5/d6d0e51fc888f692a35134336866341c08655d92614f492c6860dc45bb2c/scipy-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:37425bc9175607b0268f493d79a292c39f9d001a357bebb6b88fdfaff13f6448", size = 36510943, upload-time = "2026-02-23T00:20:50.89Z" },
- { url = "https://files.pythonhosted.org/packages/2a/fd/3be73c564e2a01e690e19cc618811540ba5354c67c8680dce3281123fb79/scipy-1.17.1-cp313-cp313-win_arm64.whl", hash = "sha256:5cf36e801231b6a2059bf354720274b7558746f3b1a4efb43fcf557ccd484a87", size = 24545621, upload-time = "2026-02-23T00:20:55.871Z" },
- { url = "https://files.pythonhosted.org/packages/6f/6b/17787db8b8114933a66f9dcc479a8272e4b4da75fe03b0c282f7b0ade8cd/scipy-1.17.1-cp313-cp313t-macosx_10_14_x86_64.whl", hash = "sha256:d59c30000a16d8edc7e64152e30220bfbd724c9bbb08368c054e24c651314f0a", size = 31936708, upload-time = "2026-02-23T00:19:58.694Z" },
- { url = "https://files.pythonhosted.org/packages/38/2e/524405c2b6392765ab1e2b722a41d5da33dc5c7b7278184a8ad29b6cb206/scipy-1.17.1-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:010f4333c96c9bb1a4516269e33cb5917b08ef2166d5556ca2fd9f082a9e6ea0", size = 28570135, upload-time = "2026-02-23T00:20:03.934Z" },
- { url = "https://files.pythonhosted.org/packages/fd/c3/5bd7199f4ea8556c0c8e39f04ccb014ac37d1468e6cfa6a95c6b3562b76e/scipy-1.17.1-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:2ceb2d3e01c5f1d83c4189737a42d9cb2fc38a6eeed225e7515eef71ad301dce", size = 20741977, upload-time = "2026-02-23T00:20:07.935Z" },
- { url = "https://files.pythonhosted.org/packages/d9/b8/8ccd9b766ad14c78386599708eb745f6b44f08400a5fd0ade7cf89b6fc93/scipy-1.17.1-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:844e165636711ef41f80b4103ed234181646b98a53c8f05da12ca5ca289134f6", size = 23029601, upload-time = "2026-02-23T00:20:12.161Z" },
- { url = "https://files.pythonhosted.org/packages/6d/a0/3cb6f4d2fb3e17428ad2880333cac878909ad1a89f678527b5328b93c1d4/scipy-1.17.1-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:158dd96d2207e21c966063e1635b1063cd7787b627b6f07305315dd73d9c679e", size = 33019667, upload-time = "2026-02-23T00:20:17.208Z" },
- { url = "https://files.pythonhosted.org/packages/f3/c3/2d834a5ac7bf3a0c806ad1508efc02dda3c8c61472a56132d7894c312dea/scipy-1.17.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:74cbb80d93260fe2ffa334efa24cb8f2f0f622a9b9febf8b483c0b865bfb3475", size = 35264159, upload-time = "2026-02-23T00:20:23.087Z" },
- { url = "https://files.pythonhosted.org/packages/4d/77/d3ed4becfdbd217c52062fafe35a72388d1bd82c2d0ba5ca19d6fcc93e11/scipy-1.17.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:dbc12c9f3d185f5c737d801da555fb74b3dcfa1a50b66a1a93e09190f41fab50", size = 35102771, upload-time = "2026-02-23T00:20:28.636Z" },
- { url = "https://files.pythonhosted.org/packages/bd/12/d19da97efde68ca1ee5538bb261d5d2c062f0c055575128f11a2730e3ac1/scipy-1.17.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:94055a11dfebe37c656e70317e1996dc197e1a15bbcc351bcdd4610e128fe1ca", size = 37665910, upload-time = "2026-02-23T00:20:34.743Z" },
- { url = "https://files.pythonhosted.org/packages/06/1c/1172a88d507a4baaf72c5a09bb6c018fe2ae0ab622e5830b703a46cc9e44/scipy-1.17.1-cp313-cp313t-win_amd64.whl", hash = "sha256:e30bdeaa5deed6bc27b4cc490823cd0347d7dae09119b8803ae576ea0ce52e4c", size = 36562980, upload-time = "2026-02-23T00:20:40.575Z" },
- { url = "https://files.pythonhosted.org/packages/70/b0/eb757336e5a76dfa7911f63252e3b7d1de00935d7705cf772db5b45ec238/scipy-1.17.1-cp313-cp313t-win_arm64.whl", hash = "sha256:a720477885a9d2411f94a93d16f9d89bad0f28ca23c3f8daa521e2dcc3f44d49", size = 24856543, upload-time = "2026-02-23T00:20:45.313Z" },
- { url = "https://files.pythonhosted.org/packages/cf/83/333afb452af6f0fd70414dc04f898647ee1423979ce02efa75c3b0f2c28e/scipy-1.17.1-cp314-cp314-macosx_10_14_x86_64.whl", hash = "sha256:a48a72c77a310327f6a3a920092fa2b8fd03d7deaa60f093038f22d98e096717", size = 31584510, upload-time = "2026-02-23T00:21:01.015Z" },
- { url = "https://files.pythonhosted.org/packages/ed/a6/d05a85fd51daeb2e4ea71d102f15b34fedca8e931af02594193ae4fd25f7/scipy-1.17.1-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:45abad819184f07240d8a696117a7aacd39787af9e0b719d00285549ed19a1e9", size = 28170131, upload-time = "2026-02-23T00:21:05.888Z" },
- { url = "https://files.pythonhosted.org/packages/db/7b/8624a203326675d7746a254083a187398090a179335b2e4a20e2ddc46e83/scipy-1.17.1-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:3fd1fcdab3ea951b610dc4cef356d416d5802991e7e32b5254828d342f7b7e0b", size = 20342032, upload-time = "2026-02-23T00:21:09.904Z" },
- { url = "https://files.pythonhosted.org/packages/c9/35/2c342897c00775d688d8ff3987aced3426858fd89d5a0e26e020b660b301/scipy-1.17.1-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:7bdf2da170b67fdf10bca777614b1c7d96ae3ca5794fd9587dce41eb2966e866", size = 22678766, upload-time = "2026-02-23T00:21:14.313Z" },
- { url = "https://files.pythonhosted.org/packages/ef/f2/7cdb8eb308a1a6ae1e19f945913c82c23c0c442a462a46480ce487fdc0ac/scipy-1.17.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:adb2642e060a6549c343603a3851ba76ef0b74cc8c079a9a58121c7ec9fe2350", size = 32957007, upload-time = "2026-02-23T00:21:19.663Z" },
- { url = "https://files.pythonhosted.org/packages/0b/2e/7eea398450457ecb54e18e9d10110993fa65561c4f3add5e8eccd2b9cd41/scipy-1.17.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eee2cfda04c00a857206a4330f0c5e3e56535494e30ca445eb19ec624ae75118", size = 35221333, upload-time = "2026-02-23T00:21:25.278Z" },
- { url = "https://files.pythonhosted.org/packages/d9/77/5b8509d03b77f093a0d52e606d3c4f79e8b06d1d38c441dacb1e26cacf46/scipy-1.17.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d2650c1fb97e184d12d8ba010493ee7b322864f7d3d00d3f9bb97d9c21de4068", size = 35042066, upload-time = "2026-02-23T00:21:31.358Z" },
- { url = "https://files.pythonhosted.org/packages/f9/df/18f80fb99df40b4070328d5ae5c596f2f00fffb50167e31439e932f29e7d/scipy-1.17.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:08b900519463543aa604a06bec02461558a6e1cef8fdbb8098f77a48a83c8118", size = 37612763, upload-time = "2026-02-23T00:21:37.247Z" },
- { url = "https://files.pythonhosted.org/packages/4b/39/f0e8ea762a764a9dc52aa7dabcfad51a354819de1f0d4652b6a1122424d6/scipy-1.17.1-cp314-cp314-win_amd64.whl", hash = "sha256:3877ac408e14da24a6196de0ddcace62092bfc12a83823e92e49e40747e52c19", size = 37290984, upload-time = "2026-02-23T00:22:35.023Z" },
- { url = "https://files.pythonhosted.org/packages/7c/56/fe201e3b0f93d1a8bcf75d3379affd228a63d7e2d80ab45467a74b494947/scipy-1.17.1-cp314-cp314-win_arm64.whl", hash = "sha256:f8885db0bc2bffa59d5c1b72fad7a6a92d3e80e7257f967dd81abb553a90d293", size = 25192877, upload-time = "2026-02-23T00:22:39.798Z" },
- { url = "https://files.pythonhosted.org/packages/96/ad/f8c414e121f82e02d76f310f16db9899c4fcde36710329502a6b2a3c0392/scipy-1.17.1-cp314-cp314t-macosx_10_14_x86_64.whl", hash = "sha256:1cc682cea2ae55524432f3cdff9e9a3be743d52a7443d0cba9017c23c87ae2f6", size = 31949750, upload-time = "2026-02-23T00:21:42.289Z" },
- { url = "https://files.pythonhosted.org/packages/7c/b0/c741e8865d61b67c81e255f4f0a832846c064e426636cd7de84e74d209be/scipy-1.17.1-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:2040ad4d1795a0ae89bfc7e8429677f365d45aa9fd5e4587cf1ea737f927b4a1", size = 28585858, upload-time = "2026-02-23T00:21:47.706Z" },
- { url = "https://files.pythonhosted.org/packages/ed/1b/3985219c6177866628fa7c2595bfd23f193ceebbe472c98a08824b9466ff/scipy-1.17.1-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:131f5aaea57602008f9822e2115029b55d4b5f7c070287699fe45c661d051e39", size = 20757723, upload-time = "2026-02-23T00:21:52.039Z" },
- { url = "https://files.pythonhosted.org/packages/c0/19/2a04aa25050d656d6f7b9e7b685cc83d6957fb101665bfd9369ca6534563/scipy-1.17.1-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:9cdc1a2fcfd5c52cfb3045feb399f7b3ce822abdde3a193a6b9a60b3cb5854ca", size = 23043098, upload-time = "2026-02-23T00:21:56.185Z" },
- { url = "https://files.pythonhosted.org/packages/86/f1/3383beb9b5d0dbddd030335bf8a8b32d4317185efe495374f134d8be6cce/scipy-1.17.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e3dcd57ab780c741fde8dc68619de988b966db759a3c3152e8e9142c26295ad", size = 33030397, upload-time = "2026-02-23T00:22:01.404Z" },
- { url = "https://files.pythonhosted.org/packages/41/68/8f21e8a65a5a03f25a79165ec9d2b28c00e66dc80546cf5eb803aeeff35b/scipy-1.17.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a9956e4d4f4a301ebf6cde39850333a6b6110799d470dbbb1e25326ac447f52a", size = 35281163, upload-time = "2026-02-23T00:22:07.024Z" },
- { url = "https://files.pythonhosted.org/packages/84/8d/c8a5e19479554007a5632ed7529e665c315ae7492b4f946b0deb39870e39/scipy-1.17.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:a4328d245944d09fd639771de275701ccadf5f781ba0ff092ad141e017eccda4", size = 35116291, upload-time = "2026-02-23T00:22:12.585Z" },
- { url = "https://files.pythonhosted.org/packages/52/52/e57eceff0e342a1f50e274264ed47497b59e6a4e3118808ee58ddda7b74a/scipy-1.17.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a77cbd07b940d326d39a1d1b37817e2ee4d79cb30e7338f3d0cddffae70fcaa2", size = 37682317, upload-time = "2026-02-23T00:22:18.513Z" },
- { url = "https://files.pythonhosted.org/packages/11/2f/b29eafe4a3fbc3d6de9662b36e028d5f039e72d345e05c250e121a230dd4/scipy-1.17.1-cp314-cp314t-win_amd64.whl", hash = "sha256:eb092099205ef62cd1782b006658db09e2fed75bffcae7cc0d44052d8aa0f484", size = 37345327, upload-time = "2026-02-23T00:22:24.442Z" },
- { url = "https://files.pythonhosted.org/packages/07/39/338d9219c4e87f3e708f18857ecd24d22a0c3094752393319553096b98af/scipy-1.17.1-cp314-cp314t-win_arm64.whl", hash = "sha256:200e1050faffacc162be6a486a984a0497866ec54149a01270adc8a59b7c7d21", size = 25489165, upload-time = "2026-02-23T00:22:29.563Z" },
+ { url = "https://files.pythonhosted.org/packages/76/27/07ee1b57b65e92645f219b37148a7e7928b82e2b5dbeccecb4dff7c64f0b/scipy-1.17.1-cp313-cp313-macosx_10_14_x86_64.whl", hash = "sha256:5e3c5c011904115f88a39308379c17f91546f77c1667cea98739fe0fccea804c", size = 31590199, upload-time = "2026-02-23T00:19:17.192Z" },
+ { url = "https://files.pythonhosted.org/packages/ec/ae/db19f8ab842e9b724bf5dbb7db29302a91f1e55bc4d04b1025d6d605a2c5/scipy-1.17.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:6fac755ca3d2c3edcb22f479fceaa241704111414831ddd3bc6056e18516892f", size = 28154001, upload-time = "2026-02-23T00:19:22.241Z" },
+ { url = "https://files.pythonhosted.org/packages/5b/58/3ce96251560107b381cbd6e8413c483bbb1228a6b919fa8652b0d4090e7f/scipy-1.17.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:7ff200bf9d24f2e4d5dc6ee8c3ac64d739d3a89e2326ba68aaf6c4a2b838fd7d", size = 20325719, upload-time = "2026-02-23T00:19:26.329Z" },
+ { url = "https://files.pythonhosted.org/packages/b2/83/15087d945e0e4d48ce2377498abf5ad171ae013232ae31d06f336e64c999/scipy-1.17.1-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:4b400bdc6f79fa02a4d86640310dde87a21fba0c979efff5248908c6f15fad1b", size = 22683595, upload-time = "2026-02-23T00:19:30.304Z" },
+ { url = "https://files.pythonhosted.org/packages/b4/e0/e58fbde4a1a594c8be8114eb4aac1a55bcd6587047efc18a61eb1f5c0d30/scipy-1.17.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2b64ca7d4aee0102a97f3ba22124052b4bd2152522355073580bf4845e2550b6", size = 32896429, upload-time = "2026-02-23T00:19:35.536Z" },
+ { url = "https://files.pythonhosted.org/packages/f5/5f/f17563f28ff03c7b6799c50d01d5d856a1d55f2676f537ca8d28c7f627cd/scipy-1.17.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:581b2264fc0aa555f3f435a5944da7504ea3a065d7029ad60e7c3d1ae09c5464", size = 35203952, upload-time = "2026-02-23T00:19:42.259Z" },
+ { url = "https://files.pythonhosted.org/packages/8d/a5/9afd17de24f657fdfe4df9a3f1ea049b39aef7c06000c13db1530d81ccca/scipy-1.17.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:beeda3d4ae615106d7094f7e7cef6218392e4465cc95d25f900bebabfded0950", size = 34979063, upload-time = "2026-02-23T00:19:47.547Z" },
+ { url = "https://files.pythonhosted.org/packages/8b/13/88b1d2384b424bf7c924f2038c1c409f8d88bb2a8d49d097861dd64a57b2/scipy-1.17.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6609bc224e9568f65064cfa72edc0f24ee6655b47575954ec6339534b2798369", size = 37598449, upload-time = "2026-02-23T00:19:53.238Z" },
+ { url = "https://files.pythonhosted.org/packages/35/e5/d6d0e51fc888f692a35134336866341c08655d92614f492c6860dc45bb2c/scipy-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:37425bc9175607b0268f493d79a292c39f9d001a357bebb6b88fdfaff13f6448", size = 36510943, upload-time = "2026-02-23T00:20:50.89Z" },
+ { url = "https://files.pythonhosted.org/packages/2a/fd/3be73c564e2a01e690e19cc618811540ba5354c67c8680dce3281123fb79/scipy-1.17.1-cp313-cp313-win_arm64.whl", hash = "sha256:5cf36e801231b6a2059bf354720274b7558746f3b1a4efb43fcf557ccd484a87", size = 24545621, upload-time = "2026-02-23T00:20:55.871Z" },
+ { url = "https://files.pythonhosted.org/packages/6f/6b/17787db8b8114933a66f9dcc479a8272e4b4da75fe03b0c282f7b0ade8cd/scipy-1.17.1-cp313-cp313t-macosx_10_14_x86_64.whl", hash = "sha256:d59c30000a16d8edc7e64152e30220bfbd724c9bbb08368c054e24c651314f0a", size = 31936708, upload-time = "2026-02-23T00:19:58.694Z" },
+ { url = "https://files.pythonhosted.org/packages/38/2e/524405c2b6392765ab1e2b722a41d5da33dc5c7b7278184a8ad29b6cb206/scipy-1.17.1-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:010f4333c96c9bb1a4516269e33cb5917b08ef2166d5556ca2fd9f082a9e6ea0", size = 28570135, upload-time = "2026-02-23T00:20:03.934Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/c3/5bd7199f4ea8556c0c8e39f04ccb014ac37d1468e6cfa6a95c6b3562b76e/scipy-1.17.1-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:2ceb2d3e01c5f1d83c4189737a42d9cb2fc38a6eeed225e7515eef71ad301dce", size = 20741977, upload-time = "2026-02-23T00:20:07.935Z" },
+ { url = "https://files.pythonhosted.org/packages/d9/b8/8ccd9b766ad14c78386599708eb745f6b44f08400a5fd0ade7cf89b6fc93/scipy-1.17.1-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:844e165636711ef41f80b4103ed234181646b98a53c8f05da12ca5ca289134f6", size = 23029601, upload-time = "2026-02-23T00:20:12.161Z" },
+ { url = "https://files.pythonhosted.org/packages/6d/a0/3cb6f4d2fb3e17428ad2880333cac878909ad1a89f678527b5328b93c1d4/scipy-1.17.1-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:158dd96d2207e21c966063e1635b1063cd7787b627b6f07305315dd73d9c679e", size = 33019667, upload-time = "2026-02-23T00:20:17.208Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/c3/2d834a5ac7bf3a0c806ad1508efc02dda3c8c61472a56132d7894c312dea/scipy-1.17.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:74cbb80d93260fe2ffa334efa24cb8f2f0f622a9b9febf8b483c0b865bfb3475", size = 35264159, upload-time = "2026-02-23T00:20:23.087Z" },
+ { url = "https://files.pythonhosted.org/packages/4d/77/d3ed4becfdbd217c52062fafe35a72388d1bd82c2d0ba5ca19d6fcc93e11/scipy-1.17.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:dbc12c9f3d185f5c737d801da555fb74b3dcfa1a50b66a1a93e09190f41fab50", size = 35102771, upload-time = "2026-02-23T00:20:28.636Z" },
+ { url = "https://files.pythonhosted.org/packages/bd/12/d19da97efde68ca1ee5538bb261d5d2c062f0c055575128f11a2730e3ac1/scipy-1.17.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:94055a11dfebe37c656e70317e1996dc197e1a15bbcc351bcdd4610e128fe1ca", size = 37665910, upload-time = "2026-02-23T00:20:34.743Z" },
+ { url = "https://files.pythonhosted.org/packages/06/1c/1172a88d507a4baaf72c5a09bb6c018fe2ae0ab622e5830b703a46cc9e44/scipy-1.17.1-cp313-cp313t-win_amd64.whl", hash = "sha256:e30bdeaa5deed6bc27b4cc490823cd0347d7dae09119b8803ae576ea0ce52e4c", size = 36562980, upload-time = "2026-02-23T00:20:40.575Z" },
+ { url = "https://files.pythonhosted.org/packages/70/b0/eb757336e5a76dfa7911f63252e3b7d1de00935d7705cf772db5b45ec238/scipy-1.17.1-cp313-cp313t-win_arm64.whl", hash = "sha256:a720477885a9d2411f94a93d16f9d89bad0f28ca23c3f8daa521e2dcc3f44d49", size = 24856543, upload-time = "2026-02-23T00:20:45.313Z" },
+ { url = "https://files.pythonhosted.org/packages/cf/83/333afb452af6f0fd70414dc04f898647ee1423979ce02efa75c3b0f2c28e/scipy-1.17.1-cp314-cp314-macosx_10_14_x86_64.whl", hash = "sha256:a48a72c77a310327f6a3a920092fa2b8fd03d7deaa60f093038f22d98e096717", size = 31584510, upload-time = "2026-02-23T00:21:01.015Z" },
+ { url = "https://files.pythonhosted.org/packages/ed/a6/d05a85fd51daeb2e4ea71d102f15b34fedca8e931af02594193ae4fd25f7/scipy-1.17.1-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:45abad819184f07240d8a696117a7aacd39787af9e0b719d00285549ed19a1e9", size = 28170131, upload-time = "2026-02-23T00:21:05.888Z" },
+ { url = "https://files.pythonhosted.org/packages/db/7b/8624a203326675d7746a254083a187398090a179335b2e4a20e2ddc46e83/scipy-1.17.1-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:3fd1fcdab3ea951b610dc4cef356d416d5802991e7e32b5254828d342f7b7e0b", size = 20342032, upload-time = "2026-02-23T00:21:09.904Z" },
+ { url = "https://files.pythonhosted.org/packages/c9/35/2c342897c00775d688d8ff3987aced3426858fd89d5a0e26e020b660b301/scipy-1.17.1-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:7bdf2da170b67fdf10bca777614b1c7d96ae3ca5794fd9587dce41eb2966e866", size = 22678766, upload-time = "2026-02-23T00:21:14.313Z" },
+ { url = "https://files.pythonhosted.org/packages/ef/f2/7cdb8eb308a1a6ae1e19f945913c82c23c0c442a462a46480ce487fdc0ac/scipy-1.17.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:adb2642e060a6549c343603a3851ba76ef0b74cc8c079a9a58121c7ec9fe2350", size = 32957007, upload-time = "2026-02-23T00:21:19.663Z" },
+ { url = "https://files.pythonhosted.org/packages/0b/2e/7eea398450457ecb54e18e9d10110993fa65561c4f3add5e8eccd2b9cd41/scipy-1.17.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eee2cfda04c00a857206a4330f0c5e3e56535494e30ca445eb19ec624ae75118", size = 35221333, upload-time = "2026-02-23T00:21:25.278Z" },
+ { url = "https://files.pythonhosted.org/packages/d9/77/5b8509d03b77f093a0d52e606d3c4f79e8b06d1d38c441dacb1e26cacf46/scipy-1.17.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d2650c1fb97e184d12d8ba010493ee7b322864f7d3d00d3f9bb97d9c21de4068", size = 35042066, upload-time = "2026-02-23T00:21:31.358Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/df/18f80fb99df40b4070328d5ae5c596f2f00fffb50167e31439e932f29e7d/scipy-1.17.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:08b900519463543aa604a06bec02461558a6e1cef8fdbb8098f77a48a83c8118", size = 37612763, upload-time = "2026-02-23T00:21:37.247Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/39/f0e8ea762a764a9dc52aa7dabcfad51a354819de1f0d4652b6a1122424d6/scipy-1.17.1-cp314-cp314-win_amd64.whl", hash = "sha256:3877ac408e14da24a6196de0ddcace62092bfc12a83823e92e49e40747e52c19", size = 37290984, upload-time = "2026-02-23T00:22:35.023Z" },
+ { url = "https://files.pythonhosted.org/packages/7c/56/fe201e3b0f93d1a8bcf75d3379affd228a63d7e2d80ab45467a74b494947/scipy-1.17.1-cp314-cp314-win_arm64.whl", hash = "sha256:f8885db0bc2bffa59d5c1b72fad7a6a92d3e80e7257f967dd81abb553a90d293", size = 25192877, upload-time = "2026-02-23T00:22:39.798Z" },
+ { url = "https://files.pythonhosted.org/packages/96/ad/f8c414e121f82e02d76f310f16db9899c4fcde36710329502a6b2a3c0392/scipy-1.17.1-cp314-cp314t-macosx_10_14_x86_64.whl", hash = "sha256:1cc682cea2ae55524432f3cdff9e9a3be743d52a7443d0cba9017c23c87ae2f6", size = 31949750, upload-time = "2026-02-23T00:21:42.289Z" },
+ { url = "https://files.pythonhosted.org/packages/7c/b0/c741e8865d61b67c81e255f4f0a832846c064e426636cd7de84e74d209be/scipy-1.17.1-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:2040ad4d1795a0ae89bfc7e8429677f365d45aa9fd5e4587cf1ea737f927b4a1", size = 28585858, upload-time = "2026-02-23T00:21:47.706Z" },
+ { url = "https://files.pythonhosted.org/packages/ed/1b/3985219c6177866628fa7c2595bfd23f193ceebbe472c98a08824b9466ff/scipy-1.17.1-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:131f5aaea57602008f9822e2115029b55d4b5f7c070287699fe45c661d051e39", size = 20757723, upload-time = "2026-02-23T00:21:52.039Z" },
+ { url = "https://files.pythonhosted.org/packages/c0/19/2a04aa25050d656d6f7b9e7b685cc83d6957fb101665bfd9369ca6534563/scipy-1.17.1-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:9cdc1a2fcfd5c52cfb3045feb399f7b3ce822abdde3a193a6b9a60b3cb5854ca", size = 23043098, upload-time = "2026-02-23T00:21:56.185Z" },
+ { url = "https://files.pythonhosted.org/packages/86/f1/3383beb9b5d0dbddd030335bf8a8b32d4317185efe495374f134d8be6cce/scipy-1.17.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e3dcd57ab780c741fde8dc68619de988b966db759a3c3152e8e9142c26295ad", size = 33030397, upload-time = "2026-02-23T00:22:01.404Z" },
+ { url = "https://files.pythonhosted.org/packages/41/68/8f21e8a65a5a03f25a79165ec9d2b28c00e66dc80546cf5eb803aeeff35b/scipy-1.17.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a9956e4d4f4a301ebf6cde39850333a6b6110799d470dbbb1e25326ac447f52a", size = 35281163, upload-time = "2026-02-23T00:22:07.024Z" },
+ { url = "https://files.pythonhosted.org/packages/84/8d/c8a5e19479554007a5632ed7529e665c315ae7492b4f946b0deb39870e39/scipy-1.17.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:a4328d245944d09fd639771de275701ccadf5f781ba0ff092ad141e017eccda4", size = 35116291, upload-time = "2026-02-23T00:22:12.585Z" },
+ { url = "https://files.pythonhosted.org/packages/52/52/e57eceff0e342a1f50e274264ed47497b59e6a4e3118808ee58ddda7b74a/scipy-1.17.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a77cbd07b940d326d39a1d1b37817e2ee4d79cb30e7338f3d0cddffae70fcaa2", size = 37682317, upload-time = "2026-02-23T00:22:18.513Z" },
+ { url = "https://files.pythonhosted.org/packages/11/2f/b29eafe4a3fbc3d6de9662b36e028d5f039e72d345e05c250e121a230dd4/scipy-1.17.1-cp314-cp314t-win_amd64.whl", hash = "sha256:eb092099205ef62cd1782b006658db09e2fed75bffcae7cc0d44052d8aa0f484", size = 37345327, upload-time = "2026-02-23T00:22:24.442Z" },
+ { url = "https://files.pythonhosted.org/packages/07/39/338d9219c4e87f3e708f18857ecd24d22a0c3094752393319553096b98af/scipy-1.17.1-cp314-cp314t-win_arm64.whl", hash = "sha256:200e1050faffacc162be6a486a984a0497866ec54149a01270adc8a59b7c7d21", size = 25489165, upload-time = "2026-02-23T00:22:29.563Z" },
]
[[package]]
@@ -1355,19 +1356,19 @@ name = "sentence-transformers"
version = "5.5.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
- { name = "huggingface-hub" },
- { name = "numpy" },
- { name = "scikit-learn" },
- { name = "scipy" },
- { name = "torch", version = "2.12.0", source = { registry = "https://download.pytorch.org/whl/cpu" }, marker = "sys_platform == 'darwin'" },
- { name = "torch", version = "2.12.0+cpu", source = { registry = "https://download.pytorch.org/whl/cpu" }, marker = "sys_platform != 'darwin'" },
- { name = "tqdm" },
- { name = "transformers" },
- { name = "typing-extensions" },
+ { name = "huggingface-hub" },
+ { name = "numpy" },
+ { name = "scikit-learn" },
+ { name = "scipy" },
+ { name = "torch", version = "2.12.0", source = { registry = "https://download.pytorch.org/whl/cpu" }, marker = "sys_platform == 'darwin'" },
+ { name = "torch", version = "2.12.0+cpu", source = { registry = "https://download.pytorch.org/whl/cpu" }, marker = "sys_platform != 'darwin'" },
+ { name = "tqdm" },
+ { name = "transformers" },
+ { name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/cf/d4/7ef93157485e978c016f49da05363c1e4e7237beb5343b64b5631101f0f1/sentence_transformers-5.5.1.tar.gz", hash = "sha256:02b7740dfc60bdbbcb6061625f5d97a5c1a4e2d3baac5f9391b912bb5eae2290", size = 445161, upload-time = "2026-05-20T07:37:44.465Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/bf/03/ee99a6b030e7a2e056547729f8a4709dd93e13d9c6f07590f74c395c4017/sentence_transformers-5.5.1-py3-none-any.whl", hash = "sha256:4fe11d433badc5282d32f7fc08bc714216b7a5aca426f9df77a45a554756deb7", size = 588887, upload-time = "2026-05-20T07:37:43.004Z" },
+ { url = "https://files.pythonhosted.org/packages/bf/03/ee99a6b030e7a2e056547729f8a4709dd93e13d9c6f07590f74c395c4017/sentence_transformers-5.5.1-py3-none-any.whl", hash = "sha256:4fe11d433badc5282d32f7fc08bc714216b7a5aca426f9df77a45a554756deb7", size = 588887, upload-time = "2026-05-20T07:37:43.004Z" },
]
[[package]]
@@ -1376,7 +1377,7 @@ version = "81.0.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/0d/1c/73e719955c59b8e424d015ab450f51c0af856ae46ea2da83eba51cc88de1/setuptools-81.0.0.tar.gz", hash = "sha256:487b53915f52501f0a79ccfd0c02c165ffe06631443a886740b91af4b7a5845a", size = 1198299, upload-time = "2026-02-06T21:10:39.601Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/e1/e3/c164c88b2e5ce7b24d667b9bd83589cf4f3520d97cad01534cd3c4f55fdb/setuptools-81.0.0-py3-none-any.whl", hash = "sha256:fdd925d5c5d9f62e4b74b30d6dd7828ce236fd6ed998a08d81de62ce5a6310d6", size = 1062021, upload-time = "2026-02-06T21:10:37.175Z" },
+ { url = "https://files.pythonhosted.org/packages/e1/e3/c164c88b2e5ce7b24d667b9bd83589cf4f3520d97cad01534cd3c4f55fdb/setuptools-81.0.0-py3-none-any.whl", hash = "sha256:fdd925d5c5d9f62e4b74b30d6dd7828ce236fd6ed998a08d81de62ce5a6310d6", size = 1062021, upload-time = "2026-02-06T21:10:37.175Z" },
]
[[package]]
@@ -1385,7 +1386,7 @@ version = "1.5.4"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" },
+ { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" },
]
[[package]]
@@ -1394,7 +1395,7 @@ version = "1.17.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" },
+ { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" },
]
[[package]]
@@ -1403,7 +1404,7 @@ version = "30.8.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/0f/64/89299aefc6ebdf4fc899f5dc14c7fcb7eb9da9290a2b4d615ae7ab884b17/sqlglot-30.8.0.tar.gz", hash = "sha256:1c5f93fb742dd9aaa75eee6bb33a637794a858b9a86375fac23a2dc0f7bc127e", size = 5869750, upload-time = "2026-05-13T09:04:38.923Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/88/4e/80705091aaf9c95e125d243f0aa871bc9f3670b4c9d963e6bad3b3dce8ff/sqlglot-30.8.0-py3-none-any.whl", hash = "sha256:af903378c331d5b72277a1b41118f07bc3e50cf4478e2d47eed12c96ee6a22a4", size = 687831, upload-time = "2026-05-13T09:04:36.336Z" },
+ { url = "https://files.pythonhosted.org/packages/88/4e/80705091aaf9c95e125d243f0aa871bc9f3670b4c9d963e6bad3b3dce8ff/sqlglot-30.8.0-py3-none-any.whl", hash = "sha256:af903378c331d5b72277a1b41118f07bc3e50cf4478e2d47eed12c96ee6a22a4", size = 687831, upload-time = "2026-05-13T09:04:36.336Z" },
]
[[package]]
@@ -1411,11 +1412,11 @@ name = "starlette"
version = "1.2.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
- { name = "anyio" },
+ { name = "anyio" },
]
sdist = { url = "https://files.pythonhosted.org/packages/c5/bf/616a066c2760f6c2b1ae3437cc28149734d069fbb46511712beae118a68c/starlette-1.2.0.tar.gz", hash = "sha256:3c5a6b23fff42492914e93890bb80cbfea72dbf37de268eec06185d62a4ca553", size = 2668923, upload-time = "2026-05-28T11:42:50.568Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/9f/85/492183764d5d01d4514be3730fdb8e228a80605783099551c51627578b5d/starlette-1.2.0-py3-none-any.whl", hash = "sha256:36e0c76ac59157e75dc4b3bdeafba97fb04eaf1878045f15dbef666a6f092ed7", size = 73213, upload-time = "2026-05-28T11:42:48.801Z" },
+ { url = "https://files.pythonhosted.org/packages/9f/85/492183764d5d01d4514be3730fdb8e228a80605783099551c51627578b5d/starlette-1.2.0-py3-none-any.whl", hash = "sha256:36e0c76ac59157e75dc4b3bdeafba97fb04eaf1878045f15dbef666a6f092ed7", size = 73213, upload-time = "2026-05-28T11:42:48.801Z" },
]
[[package]]
@@ -1423,11 +1424,11 @@ name = "sympy"
version = "1.14.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
- { name = "mpmath" },
+ { name = "mpmath" },
]
sdist = { url = "https://files.pythonhosted.org/packages/83/d3/803453b36afefb7c2bb238361cd4ae6125a569b4db67cd9e79846ba2d68c/sympy-1.14.0.tar.gz", hash = "sha256:d3d3fe8df1e5a0b42f0e7bdf50541697dbe7d23746e894990c030e2b05e72517", size = 7793921, upload-time = "2025-04-27T18:05:01.611Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl", hash = "sha256:e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5", size = 6299353, upload-time = "2025-04-27T18:04:59.103Z" },
+ { url = "https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl", hash = "sha256:e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5", size = 6299353, upload-time = "2025-04-27T18:04:59.103Z" },
]
[[package]]
@@ -1436,7 +1437,7 @@ version = "3.6.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/b7/4d/08c89e34946fce2aec4fbb45c9016efd5f4d7f24af8e5d93296e935631d8/threadpoolctl-3.6.0.tar.gz", hash = "sha256:8ab8b4aa3491d812b623328249fab5302a68d2d71745c8a4c719a2fcaba9f44e", size = 21274, upload-time = "2025-03-13T13:49:23.031Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/32/d5/f9a850d79b0851d1d4ef6456097579a9005b31fea68726a4ae5f2d82ddd9/threadpoolctl-3.6.0-py3-none-any.whl", hash = "sha256:43a0b8fd5a2928500110039e43a5eed8480b918967083ea48dc3ab9f13c4a7fb", size = 18638, upload-time = "2025-03-13T13:49:21.846Z" },
+ { url = "https://files.pythonhosted.org/packages/32/d5/f9a850d79b0851d1d4ef6456097579a9005b31fea68726a4ae5f2d82ddd9/threadpoolctl-3.6.0-py3-none-any.whl", hash = "sha256:43a0b8fd5a2928500110039e43a5eed8480b918967083ea48dc3ab9f13c4a7fb", size = 18638, upload-time = "2025-03-13T13:49:21.846Z" },
]
[[package]]
@@ -1444,25 +1445,25 @@ name = "tokenizers"
version = "0.22.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
- { name = "huggingface-hub" },
+ { name = "huggingface-hub" },
]
sdist = { url = "https://files.pythonhosted.org/packages/73/6f/f80cfef4a312e1fb34baf7d85c72d4411afde10978d4657f8cdd811d3ccc/tokenizers-0.22.2.tar.gz", hash = "sha256:473b83b915e547aa366d1eee11806deaf419e17be16310ac0a14077f1e28f917", size = 372115, upload-time = "2026-01-05T10:45:15.988Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/92/97/5dbfabf04c7e348e655e907ed27913e03db0923abb5dfdd120d7b25630e1/tokenizers-0.22.2-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:544dd704ae7238755d790de45ba8da072e9af3eea688f698b137915ae959281c", size = 3100275, upload-time = "2026-01-05T10:41:02.158Z" },
- { url = "https://files.pythonhosted.org/packages/2e/47/174dca0502ef88b28f1c9e06b73ce33500eedfac7a7692108aec220464e7/tokenizers-0.22.2-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:1e418a55456beedca4621dbab65a318981467a2b188e982a23e117f115ce5001", size = 2981472, upload-time = "2026-01-05T10:41:00.276Z" },
- { url = "https://files.pythonhosted.org/packages/d6/84/7990e799f1309a8b87af6b948f31edaa12a3ed22d11b352eaf4f4b2e5753/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2249487018adec45d6e3554c71d46eb39fa8ea67156c640f7513eb26f318cec7", size = 3290736, upload-time = "2026-01-05T10:40:32.165Z" },
- { url = "https://files.pythonhosted.org/packages/78/59/09d0d9ba94dcd5f4f1368d4858d24546b4bdc0231c2354aa31d6199f0399/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:25b85325d0815e86e0bac263506dd114578953b7b53d7de09a6485e4a160a7dd", size = 3168835, upload-time = "2026-01-05T10:40:38.847Z" },
- { url = "https://files.pythonhosted.org/packages/47/50/b3ebb4243e7160bda8d34b731e54dd8ab8b133e50775872e7a434e524c28/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bfb88f22a209ff7b40a576d5324bf8286b519d7358663db21d6246fb17eea2d5", size = 3521673, upload-time = "2026-01-05T10:40:56.614Z" },
- { url = "https://files.pythonhosted.org/packages/e0/fa/89f4cb9e08df770b57adb96f8cbb7e22695a4cb6c2bd5f0c4f0ebcf33b66/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1c774b1276f71e1ef716e5486f21e76333464f47bece56bbd554485982a9e03e", size = 3724818, upload-time = "2026-01-05T10:40:44.507Z" },
- { url = "https://files.pythonhosted.org/packages/64/04/ca2363f0bfbe3b3d36e95bf67e56a4c88c8e3362b658e616d1ac185d47f2/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:df6c4265b289083bf710dff49bc51ef252f9d5be33a45ee2bed151114a56207b", size = 3379195, upload-time = "2026-01-05T10:40:51.139Z" },
- { url = "https://files.pythonhosted.org/packages/2e/76/932be4b50ef6ccedf9d3c6639b056a967a86258c6d9200643f01269211ca/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:369cc9fc8cc10cb24143873a0d95438bb8ee257bb80c71989e3ee290e8d72c67", size = 3274982, upload-time = "2026-01-05T10:40:58.331Z" },
- { url = "https://files.pythonhosted.org/packages/1d/28/5f9f5a4cc211b69e89420980e483831bcc29dade307955cc9dc858a40f01/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:29c30b83d8dcd061078b05ae0cb94d3c710555fbb44861139f9f83dcca3dc3e4", size = 9478245, upload-time = "2026-01-05T10:41:04.053Z" },
- { url = "https://files.pythonhosted.org/packages/6c/fb/66e2da4704d6aadebf8cb39f1d6d1957df667ab24cff2326b77cda0dcb85/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:37ae80a28c1d3265bb1f22464c856bd23c02a05bb211e56d0c5301a435be6c1a", size = 9560069, upload-time = "2026-01-05T10:45:10.673Z" },
- { url = "https://files.pythonhosted.org/packages/16/04/fed398b05caa87ce9b1a1bb5166645e38196081b225059a6edaff6440fac/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:791135ee325f2336f498590eb2f11dc5c295232f288e75c99a36c5dbce63088a", size = 9899263, upload-time = "2026-01-05T10:45:12.559Z" },
- { url = "https://files.pythonhosted.org/packages/05/a1/d62dfe7376beaaf1394917e0f8e93ee5f67fea8fcf4107501db35996586b/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:38337540fbbddff8e999d59970f3c6f35a82de10053206a7562f1ea02d046fa5", size = 10033429, upload-time = "2026-01-05T10:45:14.333Z" },
- { url = "https://files.pythonhosted.org/packages/fd/18/a545c4ea42af3df6effd7d13d250ba77a0a86fb20393143bbb9a92e434d4/tokenizers-0.22.2-cp39-abi3-win32.whl", hash = "sha256:a6bf3f88c554a2b653af81f3204491c818ae2ac6fbc09e76ef4773351292bc92", size = 2502363, upload-time = "2026-01-05T10:45:20.593Z" },
- { url = "https://files.pythonhosted.org/packages/65/71/0670843133a43d43070abeb1949abfdef12a86d490bea9cd9e18e37c5ff7/tokenizers-0.22.2-cp39-abi3-win_amd64.whl", hash = "sha256:c9ea31edff2968b44a88f97d784c2f16dc0729b8b143ed004699ebca91f05c48", size = 2747786, upload-time = "2026-01-05T10:45:18.411Z" },
- { url = "https://files.pythonhosted.org/packages/72/f4/0de46cfa12cdcbcd464cc59fde36912af405696f687e53a091fb432f694c/tokenizers-0.22.2-cp39-abi3-win_arm64.whl", hash = "sha256:9ce725d22864a1e965217204946f830c37876eee3b2ba6fc6255e8e903d5fcbc", size = 2612133, upload-time = "2026-01-05T10:45:17.232Z" },
+ { url = "https://files.pythonhosted.org/packages/92/97/5dbfabf04c7e348e655e907ed27913e03db0923abb5dfdd120d7b25630e1/tokenizers-0.22.2-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:544dd704ae7238755d790de45ba8da072e9af3eea688f698b137915ae959281c", size = 3100275, upload-time = "2026-01-05T10:41:02.158Z" },
+ { url = "https://files.pythonhosted.org/packages/2e/47/174dca0502ef88b28f1c9e06b73ce33500eedfac7a7692108aec220464e7/tokenizers-0.22.2-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:1e418a55456beedca4621dbab65a318981467a2b188e982a23e117f115ce5001", size = 2981472, upload-time = "2026-01-05T10:41:00.276Z" },
+ { url = "https://files.pythonhosted.org/packages/d6/84/7990e799f1309a8b87af6b948f31edaa12a3ed22d11b352eaf4f4b2e5753/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2249487018adec45d6e3554c71d46eb39fa8ea67156c640f7513eb26f318cec7", size = 3290736, upload-time = "2026-01-05T10:40:32.165Z" },
+ { url = "https://files.pythonhosted.org/packages/78/59/09d0d9ba94dcd5f4f1368d4858d24546b4bdc0231c2354aa31d6199f0399/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:25b85325d0815e86e0bac263506dd114578953b7b53d7de09a6485e4a160a7dd", size = 3168835, upload-time = "2026-01-05T10:40:38.847Z" },
+ { url = "https://files.pythonhosted.org/packages/47/50/b3ebb4243e7160bda8d34b731e54dd8ab8b133e50775872e7a434e524c28/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bfb88f22a209ff7b40a576d5324bf8286b519d7358663db21d6246fb17eea2d5", size = 3521673, upload-time = "2026-01-05T10:40:56.614Z" },
+ { url = "https://files.pythonhosted.org/packages/e0/fa/89f4cb9e08df770b57adb96f8cbb7e22695a4cb6c2bd5f0c4f0ebcf33b66/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1c774b1276f71e1ef716e5486f21e76333464f47bece56bbd554485982a9e03e", size = 3724818, upload-time = "2026-01-05T10:40:44.507Z" },
+ { url = "https://files.pythonhosted.org/packages/64/04/ca2363f0bfbe3b3d36e95bf67e56a4c88c8e3362b658e616d1ac185d47f2/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:df6c4265b289083bf710dff49bc51ef252f9d5be33a45ee2bed151114a56207b", size = 3379195, upload-time = "2026-01-05T10:40:51.139Z" },
+ { url = "https://files.pythonhosted.org/packages/2e/76/932be4b50ef6ccedf9d3c6639b056a967a86258c6d9200643f01269211ca/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:369cc9fc8cc10cb24143873a0d95438bb8ee257bb80c71989e3ee290e8d72c67", size = 3274982, upload-time = "2026-01-05T10:40:58.331Z" },
+ { url = "https://files.pythonhosted.org/packages/1d/28/5f9f5a4cc211b69e89420980e483831bcc29dade307955cc9dc858a40f01/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:29c30b83d8dcd061078b05ae0cb94d3c710555fbb44861139f9f83dcca3dc3e4", size = 9478245, upload-time = "2026-01-05T10:41:04.053Z" },
+ { url = "https://files.pythonhosted.org/packages/6c/fb/66e2da4704d6aadebf8cb39f1d6d1957df667ab24cff2326b77cda0dcb85/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:37ae80a28c1d3265bb1f22464c856bd23c02a05bb211e56d0c5301a435be6c1a", size = 9560069, upload-time = "2026-01-05T10:45:10.673Z" },
+ { url = "https://files.pythonhosted.org/packages/16/04/fed398b05caa87ce9b1a1bb5166645e38196081b225059a6edaff6440fac/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:791135ee325f2336f498590eb2f11dc5c295232f288e75c99a36c5dbce63088a", size = 9899263, upload-time = "2026-01-05T10:45:12.559Z" },
+ { url = "https://files.pythonhosted.org/packages/05/a1/d62dfe7376beaaf1394917e0f8e93ee5f67fea8fcf4107501db35996586b/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:38337540fbbddff8e999d59970f3c6f35a82de10053206a7562f1ea02d046fa5", size = 10033429, upload-time = "2026-01-05T10:45:14.333Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/18/a545c4ea42af3df6effd7d13d250ba77a0a86fb20393143bbb9a92e434d4/tokenizers-0.22.2-cp39-abi3-win32.whl", hash = "sha256:a6bf3f88c554a2b653af81f3204491c818ae2ac6fbc09e76ef4773351292bc92", size = 2502363, upload-time = "2026-01-05T10:45:20.593Z" },
+ { url = "https://files.pythonhosted.org/packages/65/71/0670843133a43d43070abeb1949abfdef12a86d490bea9cd9e18e37c5ff7/tokenizers-0.22.2-cp39-abi3-win_amd64.whl", hash = "sha256:c9ea31edff2968b44a88f97d784c2f16dc0729b8b143ed004699ebca91f05c48", size = 2747786, upload-time = "2026-01-05T10:45:18.411Z" },
+ { url = "https://files.pythonhosted.org/packages/72/f4/0de46cfa12cdcbcd464cc59fde36912af405696f687e53a091fb432f694c/tokenizers-0.22.2-cp39-abi3-win_arm64.whl", hash = "sha256:9ce725d22864a1e965217204946f830c37876eee3b2ba6fc6255e8e903d5fcbc", size = 2612133, upload-time = "2026-01-05T10:45:17.232Z" },
]
[[package]]
@@ -1470,23 +1471,23 @@ name = "torch"
version = "2.12.0"
source = { registry = "https://download.pytorch.org/whl/cpu" }
resolution-markers = [
- "python_full_version >= '3.14' and sys_platform == 'darwin'",
- "python_full_version < '3.14' and sys_platform == 'darwin'",
+ "python_full_version >= '3.14' and sys_platform == 'darwin'",
+ "python_full_version < '3.14' and sys_platform == 'darwin'",
]
dependencies = [
- { name = "filelock", marker = "sys_platform == 'darwin'" },
- { name = "fsspec", marker = "sys_platform == 'darwin'" },
- { name = "jinja2", marker = "sys_platform == 'darwin'" },
- { name = "networkx", marker = "sys_platform == 'darwin'" },
- { name = "setuptools", marker = "sys_platform == 'darwin'" },
- { name = "sympy", marker = "sys_platform == 'darwin'" },
- { name = "typing-extensions", marker = "sys_platform == 'darwin'" },
+ { name = "filelock", marker = "sys_platform == 'darwin'" },
+ { name = "fsspec", marker = "sys_platform == 'darwin'" },
+ { name = "jinja2", marker = "sys_platform == 'darwin'" },
+ { name = "networkx", marker = "sys_platform == 'darwin'" },
+ { name = "setuptools", marker = "sys_platform == 'darwin'" },
+ { name = "sympy", marker = "sys_platform == 'darwin'" },
+ { name = "typing-extensions", marker = "sys_platform == 'darwin'" },
]
wheels = [
- { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.12.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:90dd587a5f61bfe1307148b581e2084fc5bc4a06e2b90a20e9a36b81087ff16b", upload-time = "2026-05-12T16:20:17Z" },
- { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.12.0-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:10ee1448a9f304d3b987eb4656f664ba6e4d7b410ca7a5a7c642199777a2cf88", upload-time = "2026-05-12T16:20:21Z" },
- { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.12.0-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:f7dfae4a519197dfa050e98d8e36378a0fb5899625a875c2b54445005a2e404e", upload-time = "2026-05-12T16:20:26Z" },
- { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.12.0-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:b4556715c8572758625d62b6e0ae3b1f76c440221913a6fb5e100f321fb4fb02", upload-time = "2026-05-12T16:20:31Z" },
+ { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.12.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:90dd587a5f61bfe1307148b581e2084fc5bc4a06e2b90a20e9a36b81087ff16b", upload-time = "2026-05-12T16:20:17Z" },
+ { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.12.0-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:10ee1448a9f304d3b987eb4656f664ba6e4d7b410ca7a5a7c642199777a2cf88", upload-time = "2026-05-12T16:20:21Z" },
+ { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.12.0-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:f7dfae4a519197dfa050e98d8e36378a0fb5899625a875c2b54445005a2e404e", upload-time = "2026-05-12T16:20:26Z" },
+ { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.12.0-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:b4556715c8572758625d62b6e0ae3b1f76c440221913a6fb5e100f321fb4fb02", upload-time = "2026-05-12T16:20:31Z" },
]
[[package]]
@@ -1494,40 +1495,40 @@ name = "torch"
version = "2.12.0+cpu"
source = { registry = "https://download.pytorch.org/whl/cpu" }
resolution-markers = [
- "python_full_version >= '3.14' and sys_platform == 'win32'",
- "python_full_version >= '3.14' and sys_platform == 'emscripten'",
- "python_full_version >= '3.14' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32'",
- "python_full_version < '3.14' and sys_platform == 'win32'",
- "python_full_version < '3.14' and sys_platform == 'emscripten'",
- "python_full_version < '3.14' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32'",
+ "python_full_version >= '3.14' and sys_platform == 'win32'",
+ "python_full_version >= '3.14' and sys_platform == 'emscripten'",
+ "python_full_version >= '3.14' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32'",
+ "python_full_version < '3.14' and sys_platform == 'win32'",
+ "python_full_version < '3.14' and sys_platform == 'emscripten'",
+ "python_full_version < '3.14' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32'",
]
dependencies = [
- { name = "filelock", marker = "sys_platform != 'darwin'" },
- { name = "fsspec", marker = "sys_platform != 'darwin'" },
- { name = "jinja2", marker = "sys_platform != 'darwin'" },
- { name = "networkx", marker = "sys_platform != 'darwin'" },
- { name = "setuptools", marker = "sys_platform != 'darwin'" },
- { name = "sympy", marker = "sys_platform != 'darwin'" },
- { name = "typing-extensions", marker = "sys_platform != 'darwin'" },
+ { name = "filelock", marker = "sys_platform != 'darwin'" },
+ { name = "fsspec", marker = "sys_platform != 'darwin'" },
+ { name = "jinja2", marker = "sys_platform != 'darwin'" },
+ { name = "networkx", marker = "sys_platform != 'darwin'" },
+ { name = "setuptools", marker = "sys_platform != 'darwin'" },
+ { name = "sympy", marker = "sys_platform != 'darwin'" },
+ { name = "typing-extensions", marker = "sys_platform != 'darwin'" },
]
wheels = [
- { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.12.0%2Bcpu-cp313-cp313-linux_s390x.whl", hash = "sha256:5e0da19e1c3bfdc9b92638c552579eac678354485d61fc8921b0461fd6c40449", upload-time = "2026-05-12T23:17:05Z" },
- { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.12.0%2Bcpu-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:68b7ddd4db4603a03e106e74c7098c8d8c8943d33c1e5ada009ca4cd885759c3", upload-time = "2026-05-12T23:17:12Z" },
- { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.12.0%2Bcpu-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:ada78018bdfa30d1c766596cd32d910dbf5b03424cd859231b6d2a00533de922", upload-time = "2026-05-12T23:17:20Z" },
- { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.12.0%2Bcpu-cp313-cp313-win_amd64.whl", hash = "sha256:59bc266826e683899d49ee0af9829f3eafd0a16e15b5db9dc591c8d955003b66", upload-time = "2026-05-12T23:17:27Z" },
- { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.12.0%2Bcpu-cp313-cp313-win_arm64.whl", hash = "sha256:97a5160abf3ca9d59a2cd7b4b4de89d9dfe290d36a1ac720262a55fbcee10b6c", upload-time = "2026-05-12T23:17:31Z" },
- { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.12.0%2Bcpu-cp313-cp313t-linux_s390x.whl", hash = "sha256:32b9b7a0974cd6149cb98def0a28a49d92d7c14a384273d5539da9624239e950", upload-time = "2026-05-12T23:17:36Z" },
- { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.12.0%2Bcpu-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:93ed8dc52c113580daf6124982b3232629045dccc5cd83a8f5ed478f7bac7340", upload-time = "2026-05-12T23:17:43Z" },
- { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.12.0%2Bcpu-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:6a1c86abd4ed15a0736cf2663ad69642ae5d1288c99e30346070e6241018a0a9", upload-time = "2026-05-12T23:17:54Z" },
- { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.12.0%2Bcpu-cp313-cp313t-win_amd64.whl", hash = "sha256:768dce4b7b3353795f667d1cb0dd7dfba06f570cd39539576097335e05bb71fe", upload-time = "2026-05-12T23:18:02Z" },
- { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.12.0%2Bcpu-cp314-cp314-linux_s390x.whl", hash = "sha256:ee1f329acfd0c2a1ccaa3393bcaf9857ea58759549bb2d67e271a6eab42382b3", upload-time = "2026-05-12T23:18:08Z" },
- { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.12.0%2Bcpu-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:797c066367792c92eb97cafba7fd0caa8d7455e6078a4ee880630077378dc372", upload-time = "2026-05-12T23:18:15Z" },
- { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.12.0%2Bcpu-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:a8f419ce3f25388d36e67153ec63b3a1b17059c49f5a7759a7e91ac4843660d3", upload-time = "2026-05-12T23:18:22Z" },
- { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.12.0%2Bcpu-cp314-cp314-win_amd64.whl", hash = "sha256:1dd196c43e74e7b3b526ff434e7efbdef3f3792a2efbecfc983d7dce501840d2", upload-time = "2026-05-12T23:18:30Z" },
- { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.12.0%2Bcpu-cp314-cp314t-linux_s390x.whl", hash = "sha256:d0d2080cb13c94ebc0c884d237e404490743d0f40192c8a180abf3b6b6f334cf", upload-time = "2026-05-12T23:18:35Z" },
- { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.12.0%2Bcpu-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:f7bc15972acad257723775237cdd120024cca844b8bc64701822fa596bcb7e14", upload-time = "2026-05-12T23:18:42Z" },
- { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.12.0%2Bcpu-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:4d79f961250d1763487ecbc90af019a80009f9e87cadc5366b3ec4ba5671fea6", upload-time = "2026-05-12T23:18:50Z" },
- { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.12.0%2Bcpu-cp314-cp314t-win_amd64.whl", hash = "sha256:46b8f4c41ac36bb5d5b47f5437b3de5541b313275e59c1d2aefd3bef32b0f531", upload-time = "2026-05-12T23:18:58Z" },
+ { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.12.0%2Bcpu-cp313-cp313-linux_s390x.whl", hash = "sha256:5e0da19e1c3bfdc9b92638c552579eac678354485d61fc8921b0461fd6c40449", upload-time = "2026-05-12T23:17:05Z" },
+ { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.12.0%2Bcpu-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:68b7ddd4db4603a03e106e74c7098c8d8c8943d33c1e5ada009ca4cd885759c3", upload-time = "2026-05-12T23:17:12Z" },
+ { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.12.0%2Bcpu-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:ada78018bdfa30d1c766596cd32d910dbf5b03424cd859231b6d2a00533de922", upload-time = "2026-05-12T23:17:20Z" },
+ { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.12.0%2Bcpu-cp313-cp313-win_amd64.whl", hash = "sha256:59bc266826e683899d49ee0af9829f3eafd0a16e15b5db9dc591c8d955003b66", upload-time = "2026-05-12T23:17:27Z" },
+ { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.12.0%2Bcpu-cp313-cp313-win_arm64.whl", hash = "sha256:97a5160abf3ca9d59a2cd7b4b4de89d9dfe290d36a1ac720262a55fbcee10b6c", upload-time = "2026-05-12T23:17:31Z" },
+ { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.12.0%2Bcpu-cp313-cp313t-linux_s390x.whl", hash = "sha256:32b9b7a0974cd6149cb98def0a28a49d92d7c14a384273d5539da9624239e950", upload-time = "2026-05-12T23:17:36Z" },
+ { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.12.0%2Bcpu-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:93ed8dc52c113580daf6124982b3232629045dccc5cd83a8f5ed478f7bac7340", upload-time = "2026-05-12T23:17:43Z" },
+ { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.12.0%2Bcpu-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:6a1c86abd4ed15a0736cf2663ad69642ae5d1288c99e30346070e6241018a0a9", upload-time = "2026-05-12T23:17:54Z" },
+ { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.12.0%2Bcpu-cp313-cp313t-win_amd64.whl", hash = "sha256:768dce4b7b3353795f667d1cb0dd7dfba06f570cd39539576097335e05bb71fe", upload-time = "2026-05-12T23:18:02Z" },
+ { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.12.0%2Bcpu-cp314-cp314-linux_s390x.whl", hash = "sha256:ee1f329acfd0c2a1ccaa3393bcaf9857ea58759549bb2d67e271a6eab42382b3", upload-time = "2026-05-12T23:18:08Z" },
+ { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.12.0%2Bcpu-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:797c066367792c92eb97cafba7fd0caa8d7455e6078a4ee880630077378dc372", upload-time = "2026-05-12T23:18:15Z" },
+ { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.12.0%2Bcpu-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:a8f419ce3f25388d36e67153ec63b3a1b17059c49f5a7759a7e91ac4843660d3", upload-time = "2026-05-12T23:18:22Z" },
+ { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.12.0%2Bcpu-cp314-cp314-win_amd64.whl", hash = "sha256:1dd196c43e74e7b3b526ff434e7efbdef3f3792a2efbecfc983d7dce501840d2", upload-time = "2026-05-12T23:18:30Z" },
+ { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.12.0%2Bcpu-cp314-cp314t-linux_s390x.whl", hash = "sha256:d0d2080cb13c94ebc0c884d237e404490743d0f40192c8a180abf3b6b6f334cf", upload-time = "2026-05-12T23:18:35Z" },
+ { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.12.0%2Bcpu-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:f7bc15972acad257723775237cdd120024cca844b8bc64701822fa596bcb7e14", upload-time = "2026-05-12T23:18:42Z" },
+ { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.12.0%2Bcpu-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:4d79f961250d1763487ecbc90af019a80009f9e87cadc5366b3ec4ba5671fea6", upload-time = "2026-05-12T23:18:50Z" },
+ { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.12.0%2Bcpu-cp314-cp314t-win_amd64.whl", hash = "sha256:46b8f4c41ac36bb5d5b47f5437b3de5541b313275e59c1d2aefd3bef32b0f531", upload-time = "2026-05-12T23:18:58Z" },
]
[[package]]
@@ -1535,11 +1536,11 @@ name = "tqdm"
version = "4.67.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
- { name = "colorama", marker = "sys_platform == 'win32'" },
+ { name = "colorama", marker = "sys_platform == 'win32'" },
]
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/16/e1/3079a9ff9b8e11b846c6ac5c8b5bfb7ff225eee721825310c91b3b50304f/tqdm-4.67.3-py3-none-any.whl", hash = "sha256:ee1e4c0e59148062281c49d80b25b67771a127c85fc9676d3be5f243206826bf", size = 78374, upload-time = "2026-02-03T17:35:50.982Z" },
+ { 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]]
@@ -1547,19 +1548,19 @@ name = "transformers"
version = "5.9.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
- { name = "huggingface-hub" },
- { name = "numpy" },
- { name = "packaging" },
- { name = "pyyaml" },
- { name = "regex" },
- { name = "safetensors" },
- { name = "tokenizers" },
- { name = "tqdm" },
- { name = "typer" },
+ { name = "huggingface-hub" },
+ { name = "numpy" },
+ { name = "packaging" },
+ { name = "pyyaml" },
+ { name = "regex" },
+ { name = "safetensors" },
+ { name = "tokenizers" },
+ { name = "tqdm" },
+ { name = "typer" },
]
sdist = { url = "https://files.pythonhosted.org/packages/51/58/7f843608f2e8421f86bb97060b54649be6239ec612b82bf9d41e65c26c00/transformers-5.9.0.tar.gz", hash = "sha256:25997cb8fa6053533171634b6162d7df54346530ec2aa9b42bb834e63668c842", size = 8642240, upload-time = "2026-05-20T14:50:49.278Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/02/ca/2eaa5359f2ccb8c2e1656bc26305ad0cf438aa392ce4b29ae67a315c186e/transformers-5.9.0-py3-none-any.whl", hash = "sha256:1d19509bcff7028ebc6b277d71caa712e8353778463d38764237d14b42b52788", size = 10787648, upload-time = "2026-05-20T14:50:45.337Z" },
+ { url = "https://files.pythonhosted.org/packages/02/ca/2eaa5359f2ccb8c2e1656bc26305ad0cf438aa392ce4b29ae67a315c186e/transformers-5.9.0-py3-none-any.whl", hash = "sha256:1d19509bcff7028ebc6b277d71caa712e8353778463d38764237d14b42b52788", size = 10787648, upload-time = "2026-05-20T14:50:45.337Z" },
]
[[package]]
@@ -1567,14 +1568,14 @@ name = "typer"
version = "0.25.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
- { name = "annotated-doc" },
- { name = "click" },
- { name = "rich" },
- { name = "shellingham" },
+ { name = "annotated-doc" },
+ { name = "click" },
+ { name = "rich" },
+ { name = "shellingham" },
]
sdist = { url = "https://files.pythonhosted.org/packages/e4/51/9aed62104cea109b820bbd6c14245af756112017d309da813ef107d42e7e/typer-0.25.1.tar.gz", hash = "sha256:9616eb8853a09ffeabab1698952f33c6f29ffdbceb4eaeecf571880e8d7664cc", size = 122276, upload-time = "2026-04-30T19:32:16.964Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/3f/f9/2b3ff4e56e5fa7debfaf9eb135d0da96f3e9a1d5b27222223c7296336e5f/typer-0.25.1-py3-none-any.whl", hash = "sha256:75caa44ed46a03fb2dab8808753ffacdbfea88495e74c85a28c5eefcf5f39c89", size = 58409, upload-time = "2026-04-30T19:32:18.271Z" },
+ { url = "https://files.pythonhosted.org/packages/3f/f9/2b3ff4e56e5fa7debfaf9eb135d0da96f3e9a1d5b27222223c7296336e5f/typer-0.25.1-py3-none-any.whl", hash = "sha256:75caa44ed46a03fb2dab8808753ffacdbfea88495e74c85a28c5eefcf5f39c89", size = 58409, upload-time = "2026-04-30T19:32:18.271Z" },
]
[[package]]
@@ -1583,7 +1584,7 @@ version = "4.15.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" },
+ { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" },
]
[[package]]
@@ -1591,11 +1592,11 @@ name = "typing-inspection"
version = "0.4.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
- { name = "typing-extensions" },
+ { name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" },
+ { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" },
]
[[package]]
@@ -1604,7 +1605,7 @@ version = "2026.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/ba/19/1b9b0e29f30c6d35cb345486df41110984ea67ae69dddbc0e8a100999493/tzdata-2026.2.tar.gz", hash = "sha256:9173fde7d80d9018e02a662e168e5a2d04f87c41ea174b139fbef642eda62d10", size = 198254, upload-time = "2026-04-24T15:22:08.651Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/ce/e4/dccd7f47c4b64213ac01ef921a1337ee6e30e8c6466046018326977efd95/tzdata-2026.2-py2.py3-none-any.whl", hash = "sha256:bbe9af844f658da81a5f95019480da3a89415801f6cc966806612cc7169bffe7", size = 349321, upload-time = "2026-04-24T15:22:05.876Z" },
+ { url = "https://files.pythonhosted.org/packages/ce/e4/dccd7f47c4b64213ac01ef921a1337ee6e30e8c6466046018326977efd95/tzdata-2026.2-py2.py3-none-any.whl", hash = "sha256:bbe9af844f658da81a5f95019480da3a89415801f6cc966806612cc7169bffe7", size = 349321, upload-time = "2026-04-24T15:22:05.876Z" },
]
[[package]]
@@ -1613,7 +1614,7 @@ version = "2.7.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" },
+ { url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" },
]
[[package]]
@@ -1621,23 +1622,23 @@ name = "uvicorn"
version = "0.48.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
- { name = "click" },
- { name = "h11" },
+ { name = "click" },
+ { name = "h11" },
]
sdist = { url = "https://files.pythonhosted.org/packages/e6/bf/f6544ba992ddb9a6077343a576f9844f7f8f06ab819aefd00206e9255f18/uvicorn-0.48.0.tar.gz", hash = "sha256:a5504207195d08c2511bf9125ede5ac4a4b71725d519e758d01dcf0bc2d31c37", size = 91074, upload-time = "2026-05-24T12:08:41.925Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/01/be/72532be3da7acc5fdfbccdb95215cd04f995a0886532a5b423f929cda4cc/uvicorn-0.48.0-py3-none-any.whl", hash = "sha256:48097851328b87ec36117d3d575234519eb58c2b22d79666e9bbc6c49a761dad", size = 71410, upload-time = "2026-05-24T12:08:40.258Z" },
+ { url = "https://files.pythonhosted.org/packages/01/be/72532be3da7acc5fdfbccdb95215cd04f995a0886532a5b423f929cda4cc/uvicorn-0.48.0-py3-none-any.whl", hash = "sha256:48097851328b87ec36117d3d575234519eb58c2b22d79666e9bbc6c49a761dad", size = 71410, upload-time = "2026-05-24T12:08:40.258Z" },
]
[package.optional-dependencies]
standard = [
- { name = "colorama", marker = "sys_platform == 'win32'" },
- { name = "httptools" },
- { name = "python-dotenv" },
- { name = "pyyaml" },
- { name = "uvloop", marker = "platform_python_implementation != 'PyPy' and sys_platform != 'cygwin' and sys_platform != 'win32'" },
- { name = "watchfiles" },
- { name = "websockets" },
+ { name = "colorama", marker = "sys_platform == 'win32'" },
+ { name = "httptools" },
+ { name = "python-dotenv" },
+ { name = "pyyaml" },
+ { name = "uvloop", marker = "platform_python_implementation != 'PyPy' and sys_platform != 'cygwin' and sys_platform != 'win32'" },
+ { name = "watchfiles" },
+ { name = "websockets" },
]
[[package]]
@@ -1646,24 +1647,24 @@ version = "0.22.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/06/f0/18d39dbd1971d6d62c4629cc7fa67f74821b0dc1f5a77af43719de7936a7/uvloop-0.22.1.tar.gz", hash = "sha256:6c84bae345b9147082b17371e3dd5d42775bddce91f885499017f4607fdaf39f", size = 2443250, upload-time = "2025-10-16T22:17:19.342Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/89/8c/182a2a593195bfd39842ea68ebc084e20c850806117213f5a299dfc513d9/uvloop-0.22.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:561577354eb94200d75aca23fbde86ee11be36b00e52a4eaf8f50fb0c86b7705", size = 1358611, upload-time = "2025-10-16T22:16:36.833Z" },
- { url = "https://files.pythonhosted.org/packages/d2/14/e301ee96a6dc95224b6f1162cd3312f6d1217be3907b79173b06785f2fe7/uvloop-0.22.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1cdf5192ab3e674ca26da2eada35b288d2fa49fdd0f357a19f0e7c4e7d5077c8", size = 751811, upload-time = "2025-10-16T22:16:38.275Z" },
- { url = "https://files.pythonhosted.org/packages/b7/02/654426ce265ac19e2980bfd9ea6590ca96a56f10c76e63801a2df01c0486/uvloop-0.22.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e2ea3d6190a2968f4a14a23019d3b16870dd2190cd69c8180f7c632d21de68d", size = 4288562, upload-time = "2025-10-16T22:16:39.375Z" },
- { url = "https://files.pythonhosted.org/packages/15/c0/0be24758891ef825f2065cd5db8741aaddabe3e248ee6acc5e8a80f04005/uvloop-0.22.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0530a5fbad9c9e4ee3f2b33b148c6a64d47bbad8000ea63704fa8260f4cf728e", size = 4366890, upload-time = "2025-10-16T22:16:40.547Z" },
- { url = "https://files.pythonhosted.org/packages/d2/53/8369e5219a5855869bcee5f4d317f6da0e2c669aecf0ef7d371e3d084449/uvloop-0.22.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bc5ef13bbc10b5335792360623cc378d52d7e62c2de64660616478c32cd0598e", size = 4119472, upload-time = "2025-10-16T22:16:41.694Z" },
- { url = "https://files.pythonhosted.org/packages/f8/ba/d69adbe699b768f6b29a5eec7b47dd610bd17a69de51b251126a801369ea/uvloop-0.22.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1f38ec5e3f18c8a10ded09742f7fb8de0108796eb673f30ce7762ce1b8550cad", size = 4239051, upload-time = "2025-10-16T22:16:43.224Z" },
- { url = "https://files.pythonhosted.org/packages/90/cd/b62bdeaa429758aee8de8b00ac0dd26593a9de93d302bff3d21439e9791d/uvloop-0.22.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3879b88423ec7e97cd4eba2a443aa26ed4e59b45e6b76aabf13fe2f27023a142", size = 1362067, upload-time = "2025-10-16T22:16:44.503Z" },
- { url = "https://files.pythonhosted.org/packages/0d/f8/a132124dfda0777e489ca86732e85e69afcd1ff7686647000050ba670689/uvloop-0.22.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4baa86acedf1d62115c1dc6ad1e17134476688f08c6efd8a2ab076e815665c74", size = 752423, upload-time = "2025-10-16T22:16:45.968Z" },
- { url = "https://files.pythonhosted.org/packages/a3/94/94af78c156f88da4b3a733773ad5ba0b164393e357cc4bd0ab2e2677a7d6/uvloop-0.22.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:297c27d8003520596236bdb2335e6b3f649480bd09e00d1e3a99144b691d2a35", size = 4272437, upload-time = "2025-10-16T22:16:47.451Z" },
- { url = "https://files.pythonhosted.org/packages/b5/35/60249e9fd07b32c665192cec7af29e06c7cd96fa1d08b84f012a56a0b38e/uvloop-0.22.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c1955d5a1dd43198244d47664a5858082a3239766a839b2102a269aaff7a4e25", size = 4292101, upload-time = "2025-10-16T22:16:49.318Z" },
- { url = "https://files.pythonhosted.org/packages/02/62/67d382dfcb25d0a98ce73c11ed1a6fba5037a1a1d533dcbb7cab033a2636/uvloop-0.22.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b31dc2fccbd42adc73bc4e7cdbae4fc5086cf378979e53ca5d0301838c5682c6", size = 4114158, upload-time = "2025-10-16T22:16:50.517Z" },
- { url = "https://files.pythonhosted.org/packages/f0/7a/f1171b4a882a5d13c8b7576f348acfe6074d72eaf52cccef752f748d4a9f/uvloop-0.22.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:93f617675b2d03af4e72a5333ef89450dfaa5321303ede6e67ba9c9d26878079", size = 4177360, upload-time = "2025-10-16T22:16:52.646Z" },
- { url = "https://files.pythonhosted.org/packages/79/7b/b01414f31546caf0919da80ad57cbfe24c56b151d12af68cee1b04922ca8/uvloop-0.22.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:37554f70528f60cad66945b885eb01f1bb514f132d92b6eeed1c90fd54ed6289", size = 1454790, upload-time = "2025-10-16T22:16:54.355Z" },
- { url = "https://files.pythonhosted.org/packages/d4/31/0bb232318dd838cad3fa8fb0c68c8b40e1145b32025581975e18b11fab40/uvloop-0.22.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:b76324e2dc033a0b2f435f33eb88ff9913c156ef78e153fb210e03c13da746b3", size = 796783, upload-time = "2025-10-16T22:16:55.906Z" },
- { url = "https://files.pythonhosted.org/packages/42/38/c9b09f3271a7a723a5de69f8e237ab8e7803183131bc57c890db0b6bb872/uvloop-0.22.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:badb4d8e58ee08dad957002027830d5c3b06aea446a6a3744483c2b3b745345c", size = 4647548, upload-time = "2025-10-16T22:16:57.008Z" },
- { url = "https://files.pythonhosted.org/packages/c1/37/945b4ca0ac27e3dc4952642d4c900edd030b3da6c9634875af6e13ae80e5/uvloop-0.22.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b91328c72635f6f9e0282e4a57da7470c7350ab1c9f48546c0f2866205349d21", size = 4467065, upload-time = "2025-10-16T22:16:58.206Z" },
- { url = "https://files.pythonhosted.org/packages/97/cc/48d232f33d60e2e2e0b42f4e73455b146b76ebe216487e862700457fbf3c/uvloop-0.22.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:daf620c2995d193449393d6c62131b3fbd40a63bf7b307a1527856ace637fe88", size = 4328384, upload-time = "2025-10-16T22:16:59.36Z" },
- { url = "https://files.pythonhosted.org/packages/e4/16/c1fd27e9549f3c4baf1dc9c20c456cd2f822dbf8de9f463824b0c0357e06/uvloop-0.22.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6cde23eeda1a25c75b2e07d39970f3374105d5eafbaab2a4482be82f272d5a5e", size = 4296730, upload-time = "2025-10-16T22:17:00.744Z" },
+ { url = "https://files.pythonhosted.org/packages/89/8c/182a2a593195bfd39842ea68ebc084e20c850806117213f5a299dfc513d9/uvloop-0.22.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:561577354eb94200d75aca23fbde86ee11be36b00e52a4eaf8f50fb0c86b7705", size = 1358611, upload-time = "2025-10-16T22:16:36.833Z" },
+ { url = "https://files.pythonhosted.org/packages/d2/14/e301ee96a6dc95224b6f1162cd3312f6d1217be3907b79173b06785f2fe7/uvloop-0.22.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1cdf5192ab3e674ca26da2eada35b288d2fa49fdd0f357a19f0e7c4e7d5077c8", size = 751811, upload-time = "2025-10-16T22:16:38.275Z" },
+ { url = "https://files.pythonhosted.org/packages/b7/02/654426ce265ac19e2980bfd9ea6590ca96a56f10c76e63801a2df01c0486/uvloop-0.22.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e2ea3d6190a2968f4a14a23019d3b16870dd2190cd69c8180f7c632d21de68d", size = 4288562, upload-time = "2025-10-16T22:16:39.375Z" },
+ { url = "https://files.pythonhosted.org/packages/15/c0/0be24758891ef825f2065cd5db8741aaddabe3e248ee6acc5e8a80f04005/uvloop-0.22.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0530a5fbad9c9e4ee3f2b33b148c6a64d47bbad8000ea63704fa8260f4cf728e", size = 4366890, upload-time = "2025-10-16T22:16:40.547Z" },
+ { url = "https://files.pythonhosted.org/packages/d2/53/8369e5219a5855869bcee5f4d317f6da0e2c669aecf0ef7d371e3d084449/uvloop-0.22.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bc5ef13bbc10b5335792360623cc378d52d7e62c2de64660616478c32cd0598e", size = 4119472, upload-time = "2025-10-16T22:16:41.694Z" },
+ { url = "https://files.pythonhosted.org/packages/f8/ba/d69adbe699b768f6b29a5eec7b47dd610bd17a69de51b251126a801369ea/uvloop-0.22.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1f38ec5e3f18c8a10ded09742f7fb8de0108796eb673f30ce7762ce1b8550cad", size = 4239051, upload-time = "2025-10-16T22:16:43.224Z" },
+ { url = "https://files.pythonhosted.org/packages/90/cd/b62bdeaa429758aee8de8b00ac0dd26593a9de93d302bff3d21439e9791d/uvloop-0.22.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3879b88423ec7e97cd4eba2a443aa26ed4e59b45e6b76aabf13fe2f27023a142", size = 1362067, upload-time = "2025-10-16T22:16:44.503Z" },
+ { url = "https://files.pythonhosted.org/packages/0d/f8/a132124dfda0777e489ca86732e85e69afcd1ff7686647000050ba670689/uvloop-0.22.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4baa86acedf1d62115c1dc6ad1e17134476688f08c6efd8a2ab076e815665c74", size = 752423, upload-time = "2025-10-16T22:16:45.968Z" },
+ { url = "https://files.pythonhosted.org/packages/a3/94/94af78c156f88da4b3a733773ad5ba0b164393e357cc4bd0ab2e2677a7d6/uvloop-0.22.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:297c27d8003520596236bdb2335e6b3f649480bd09e00d1e3a99144b691d2a35", size = 4272437, upload-time = "2025-10-16T22:16:47.451Z" },
+ { url = "https://files.pythonhosted.org/packages/b5/35/60249e9fd07b32c665192cec7af29e06c7cd96fa1d08b84f012a56a0b38e/uvloop-0.22.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c1955d5a1dd43198244d47664a5858082a3239766a839b2102a269aaff7a4e25", size = 4292101, upload-time = "2025-10-16T22:16:49.318Z" },
+ { url = "https://files.pythonhosted.org/packages/02/62/67d382dfcb25d0a98ce73c11ed1a6fba5037a1a1d533dcbb7cab033a2636/uvloop-0.22.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b31dc2fccbd42adc73bc4e7cdbae4fc5086cf378979e53ca5d0301838c5682c6", size = 4114158, upload-time = "2025-10-16T22:16:50.517Z" },
+ { url = "https://files.pythonhosted.org/packages/f0/7a/f1171b4a882a5d13c8b7576f348acfe6074d72eaf52cccef752f748d4a9f/uvloop-0.22.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:93f617675b2d03af4e72a5333ef89450dfaa5321303ede6e67ba9c9d26878079", size = 4177360, upload-time = "2025-10-16T22:16:52.646Z" },
+ { url = "https://files.pythonhosted.org/packages/79/7b/b01414f31546caf0919da80ad57cbfe24c56b151d12af68cee1b04922ca8/uvloop-0.22.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:37554f70528f60cad66945b885eb01f1bb514f132d92b6eeed1c90fd54ed6289", size = 1454790, upload-time = "2025-10-16T22:16:54.355Z" },
+ { url = "https://files.pythonhosted.org/packages/d4/31/0bb232318dd838cad3fa8fb0c68c8b40e1145b32025581975e18b11fab40/uvloop-0.22.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:b76324e2dc033a0b2f435f33eb88ff9913c156ef78e153fb210e03c13da746b3", size = 796783, upload-time = "2025-10-16T22:16:55.906Z" },
+ { url = "https://files.pythonhosted.org/packages/42/38/c9b09f3271a7a723a5de69f8e237ab8e7803183131bc57c890db0b6bb872/uvloop-0.22.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:badb4d8e58ee08dad957002027830d5c3b06aea446a6a3744483c2b3b745345c", size = 4647548, upload-time = "2025-10-16T22:16:57.008Z" },
+ { url = "https://files.pythonhosted.org/packages/c1/37/945b4ca0ac27e3dc4952642d4c900edd030b3da6c9634875af6e13ae80e5/uvloop-0.22.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b91328c72635f6f9e0282e4a57da7470c7350ab1c9f48546c0f2866205349d21", size = 4467065, upload-time = "2025-10-16T22:16:58.206Z" },
+ { url = "https://files.pythonhosted.org/packages/97/cc/48d232f33d60e2e2e0b42f4e73455b146b76ebe216487e862700457fbf3c/uvloop-0.22.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:daf620c2995d193449393d6c62131b3fbd40a63bf7b307a1527856ace637fe88", size = 4328384, upload-time = "2025-10-16T22:16:59.36Z" },
+ { url = "https://files.pythonhosted.org/packages/e4/16/c1fd27e9549f3c4baf1dc9c20c456cd2f822dbf8de9f463824b0c0357e06/uvloop-0.22.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6cde23eeda1a25c75b2e07d39970f3374105d5eafbaab2a4482be82f272d5a5e", size = 4296730, upload-time = "2025-10-16T22:17:00.744Z" },
]
[[package]]
@@ -1671,14 +1672,14 @@ name = "virtualenv"
version = "21.4.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
- { name = "distlib" },
- { name = "filelock" },
- { name = "platformdirs" },
- { name = "python-discovery" },
+ { name = "distlib" },
+ { name = "filelock" },
+ { name = "platformdirs" },
+ { name = "python-discovery" },
]
sdist = { url = "https://files.pythonhosted.org/packages/95/f0/b47ecf438211a25a97f8f0e4b23c22bc2496ebfea18dd6ec16210f09cc36/virtualenv-21.4.1.tar.gz", hash = "sha256:2ca543c713b72840ceffd94e9bdedfbd09a661defa1f7f69e5429ad4059442e2", size = 7613344, upload-time = "2026-05-28T04:12:49.905Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/ff/dc/ac4f3a987a87e1a18556896f257c4e15c95ed157b7975347ec6b313b75ce/virtualenv-21.4.1-py3-none-any.whl", hash = "sha256:caf4ff72d1b4039057f41d8e8466e859513d67c0400d9c6b62c02c9d1ebc3e12", size = 7594078, upload-time = "2026-05-28T04:12:47.686Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/dc/ac4f3a987a87e1a18556896f257c4e15c95ed157b7975347ec6b313b75ce/virtualenv-21.4.1-py3-none-any.whl", hash = "sha256:caf4ff72d1b4039057f41d8e8466e859513d67c0400d9c6b62c02c9d1ebc3e12", size = 7594078, upload-time = "2026-05-28T04:12:47.686Z" },
]
[[package]]
@@ -1686,71 +1687,71 @@ name = "watchfiles"
version = "1.2.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
- { name = "anyio" },
+ { name = "anyio" },
]
sdist = { url = "https://files.pythonhosted.org/packages/cd/41/5e1a4bb12aac5f1493fa1bdc11154eca3b258ca4eba65d39c473fe19d8e9/watchfiles-1.2.0.tar.gz", hash = "sha256:c995fba777f1ea992f090f9236e9284cf7a5d1a0130dd5a3d82c598cacd76838", size = 108252, upload-time = "2026-05-18T04:32:04.251Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/d1/4d/70a7feced9f87e2ff26dba42667290f41694fc64646c67261fbb8cab5d5c/watchfiles-1.2.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:01ea8d66f0693b9b60a6541c8d10263091ca9a9060d242f3c1f3143f9aad2c98", size = 399730, upload-time = "2026-05-18T04:31:38.162Z" },
- { url = "https://files.pythonhosted.org/packages/31/3a/0da302f2307aee316922806ebd5726c542cbd787c938271cf14a074c7daf/watchfiles-1.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7ba0480b9a74af058f43b337e937a451e109295c420916d68ad24e3dc02f5e44", size = 392842, upload-time = "2026-05-18T04:30:27.051Z" },
- { url = "https://files.pythonhosted.org/packages/db/ef/d5bdb705c224dbc256aa0c1ec47bf4e61ec52558f2afb44a71a1fe4d7015/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4f34e26a19f91f710c08e0183429f0d1d15df734e6bc78c31e77b9ea9c433658", size = 452989, upload-time = "2026-05-18T04:31:11.945Z" },
- { url = "https://files.pythonhosted.org/packages/71/29/5495f2c1661949ef7a35e4d71111d129cfe7606414a26887a919d0a55406/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b4e77f6a55f858504069abd35d336a637555c09bca453dde1ee1e5ada8a6a1fb", size = 458978, upload-time = "2026-05-18T04:30:52.606Z" },
- { url = "https://files.pythonhosted.org/packages/d5/8c/7f9c07c433811c2fffd93e13fdfb7135de9aab5f2ae41be08960fa0047dc/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0cb4d80e212f116474a545c21c912b445f16bb0cef9e6a73a498164223e14e2f", size = 490248, upload-time = "2026-05-18T04:31:36.003Z" },
- { url = "https://files.pythonhosted.org/packages/3c/11/d93632febc52fbc21be90231bb7c17fd5387f46c9076fd40a5f9c2ae6910/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b974946a10af379d425e2eef5b62f5c6ebeaccf91d45eaad6f5b27ecd4f91aa0", size = 571847, upload-time = "2026-05-18T04:31:10.862Z" },
- { url = "https://files.pythonhosted.org/packages/55/b4/383173e73aabb07ad1d9c7aa859d95437ac46a6d6a1e11005facda0c9d19/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:86bc13c25a8d1fcd70b51d0ce7c9b65e90de5666fcbfd3e34957cc73ee19aeb5", size = 465974, upload-time = "2026-05-18T04:30:17.006Z" },
- { url = "https://files.pythonhosted.org/packages/a7/6c/89b1a230a78f57c52dd8893adb1f92f94411721b6ec12596c56d98c74356/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca148d73dea36c9763aaa351e4d7a51780ec1584217c45276f4fe8239c768b71", size = 454782, upload-time = "2026-05-18T04:30:35.656Z" },
- { url = "https://files.pythonhosted.org/packages/24/62/1732118367cfff0a9fce3bf62ff4bfded09ef5df21d9d446b858b3f70a96/watchfiles-1.2.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:c525543d91961c6955b2636b308569e84a1d1c5f5f2932041ab9ef46422f43e3", size = 465182, upload-time = "2026-05-18T04:30:20.846Z" },
- { url = "https://files.pythonhosted.org/packages/28/96/716f7e5f51339bf22963f3345f9f27d7f3b30e2eadc597e257c881dd3c53/watchfiles-1.2.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:a204794696ffb8f9b10fba6f7cb5216d42f3b2b71860ccac6b6e42f5f10973b0", size = 629841, upload-time = "2026-05-18T04:31:05.397Z" },
- { url = "https://files.pythonhosted.org/packages/4c/fe/c40783950fd771ccf66ab3ec2722d188a9af1c7f96c6e811f36e40c6e03f/watchfiles-1.2.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:10d86db20695afe7997ac9e1717637d6714a8d0220458c33f3d2061f54cec427", size = 658028, upload-time = "2026-05-18T04:31:48.22Z" },
- { url = "https://files.pythonhosted.org/packages/71/72/4508db1856d1d87fcbb3b63f4839bab1b5682cb0e8d224d122263c09654a/watchfiles-1.2.0-cp313-cp313-win32.whl", hash = "sha256:eb283ee99e21ad6443c8cdb06ac5b34b1308c329cbdf03fa02b445363714c799", size = 275183, upload-time = "2026-05-18T04:30:59.57Z" },
- { url = "https://files.pythonhosted.org/packages/f9/36/14b76ca57652e5cc5fd1c11f32a261292c08a0d19a00351013c2549cbfb2/watchfiles-1.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:a0f27f01bee51861392bb6b7c4fdb290b27d1eb194e9e28788d68102a0e898d9", size = 288059, upload-time = "2026-05-18T04:32:07.937Z" },
- { url = "https://files.pythonhosted.org/packages/1b/8d/0a85e395398d8d20fadfe5c5d32c726eee17a519e78fb356f2cf7531bffe/watchfiles-1.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:3651aa7058595e9cfb75d35dd5ada2bf9f48a5b8a0f3562821d3e210c507e077", size = 280186, upload-time = "2026-05-18T04:31:54.484Z" },
- { url = "https://files.pythonhosted.org/packages/37/68/36db056f1fdcc5f07302f56e631774d6835bcd6fa3ace402304621d5f9e5/watchfiles-1.2.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:faea288b6f0ab1902ef08f4ca6de005dccf856c4e0c4f21b8c5fce02d90a1b08", size = 399031, upload-time = "2026-05-18T04:30:44.576Z" },
- { url = "https://files.pythonhosted.org/packages/c1/64/01a9d6f66a82a5c101ce939274106cc72759d62427e153f01edd2b9f87c2/watchfiles-1.2.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:01859b11fd9fbca670f4d5da00fbac282cfea9bd67a2125d8b2833a3b5617ea9", size = 391205, upload-time = "2026-05-18T04:30:25.413Z" },
- { url = "https://files.pythonhosted.org/packages/84/2c/0a44fe058cb4bb7b8ede6b6670698bbb7c0400740e378d00022189b7b31d/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fff610d7bb2256a317bb1e96f0d7862c7aa8076733ee5df0fd41bbe76a24a4f4", size = 451892, upload-time = "2026-05-18T04:32:14.005Z" },
- { url = "https://files.pythonhosted.org/packages/67/a1/351e0d56cd35e6488b5c8b4fb11a809a5bc923e8fe8fed9faf8920be0c89/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b141a4891c995a039cd89e9a49e62df1dc8a559a5d1a6e4c7106d16c12777a55", size = 458867, upload-time = "2026-05-18T04:31:22.279Z" },
- { url = "https://files.pythonhosted.org/packages/d5/7d/9d09605187f1b838998624049fcf8bf47b73c1a3b76901fcac1782f62277/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f22943b7770483f6ea0721c6b11d022947a98eb0acae14694de034f4d0d38925", size = 490217, upload-time = "2026-05-18T04:31:43.657Z" },
- { url = "https://files.pythonhosted.org/packages/60/5d/a17a16eccb182f04188cd308ec24b1a71a9b5c4e7098269cf35d9fa56d02/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1bc6195825b7dcd217968bb1f801a60fd4c16e8eeab5bedc7fe917d7d5995ab4", size = 571458, upload-time = "2026-05-18T04:32:11.875Z" },
- { url = "https://files.pythonhosted.org/packages/d3/3d/4dd457062083ab1938e5dfd45032eb425cee2ac817287ca8ff4356183e5d/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d4a4b147f5dca2a5d325a06a832fb43f345751adfbc63204aec30e0d9ca965a2", size = 464707, upload-time = "2026-05-18T04:30:43.492Z" },
- { url = "https://files.pythonhosted.org/packages/c6/71/ea8c57b128f5383de74d0c7d2d9c57ad7c9a65a930c451bd25d524b295b7/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4543579a9bdb0c9560039b4ffddbdb39545707659fbc430ce4c10f3f68d557f9", size = 454663, upload-time = "2026-05-18T04:30:16.061Z" },
- { url = "https://files.pythonhosted.org/packages/53/fd/2e812bf938406d7db351f0703ddd3fc6c061cf30d96153a77bc79a943a44/watchfiles-1.2.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:20aa0e708b920bde876a4aa82dc7dd6ebea228a63a67cda6632c2fc87b787efa", size = 463537, upload-time = "2026-05-18T04:31:44.9Z" },
- { url = "https://files.pythonhosted.org/packages/86/56/d17a7f1dd1bc3035f1072694a551301272f1739c2d8e319c927cb9e29b38/watchfiles-1.2.0-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:d413349d565dab74297f2a63e84a097936be69bf8f3b3801f27f380e32040f44", size = 629194, upload-time = "2026-05-18T04:31:14.141Z" },
- { url = "https://files.pythonhosted.org/packages/be/06/f1ff66bf5cae50aa4062779a0ecd0bbaf15e466195719074078947d9a17d/watchfiles-1.2.0-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:f28b2725eb8cce327b9b3ab02415c853011dc55c95832fe90de6bc56f5315f72", size = 656194, upload-time = "2026-05-18T04:31:47.14Z" },
- { url = "https://files.pythonhosted.org/packages/e7/54/a9c7ea9a82a4ac65e7004c0a03920b5cdd2f9c3b678757d9cd425aa51d53/watchfiles-1.2.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:b8c8358484d5fa12ef34f05b7f4168eaf1932f408725ff6d023c33ec17bd79d4", size = 400205, upload-time = "2026-05-18T04:32:05.153Z" },
- { url = "https://files.pythonhosted.org/packages/aa/5d/c9ab3534374a4a67450696905d6ef16a04405448b8dc52bd752ae50423d4/watchfiles-1.2.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9f04b092229ad2c50126dd3c922c8822e51e605993764a33058d4a791ab42281", size = 392508, upload-time = "2026-05-18T04:30:54.849Z" },
- { url = "https://files.pythonhosted.org/packages/26/ca/1ad30103535cf0cecd7b993e8d50edc5351b1820e38f2d22e3df58962feb/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7a7ce236284f002a156f70add88efe5c70879cccbb658be0822c54b1306fc09d", size = 452448, upload-time = "2026-05-18T04:30:53.727Z" },
- { url = "https://files.pythonhosted.org/packages/37/a1/ceee2cdf2afbd715fa07758d39c9859513eae411b23196f7fd039e5feedd/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b9909cc2b48468b575eefa944919e1fe8a36c5849d5c7c168f80a8c1db69398e", size = 459605, upload-time = "2026-05-18T04:30:23.312Z" },
- { url = "https://files.pythonhosted.org/packages/e8/f6/421e30fd1cb3907a84ed92ab3f1983e37ba2dca015e9a894a048418417a2/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a37faaed405c67e28e6be45a1fa4f206ef5a2860f27c237db9fa30704c38242", size = 490757, upload-time = "2026-05-18T04:30:47.358Z" },
- { url = "https://files.pythonhosted.org/packages/41/b0/55ed1b97ed08be7bba6f9a541cac15f2a858e1d74d2b07b6da70a82aab00/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9649193aa27bd9ff2e80ff29bfaa93085496c7a3a377592823cc58b77ee88add", size = 568672, upload-time = "2026-05-18T04:30:38.915Z" },
- { url = "https://files.pythonhosted.org/packages/d1/cf/d8ae8a80dd7bafab395ea7681c10237311bbf34d37704a8c744e7cf31fc7/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e4ff8e37f99cf1da89e255e07c9c4b37c214038c4283707bdec308cb1b0ea1f", size = 464197, upload-time = "2026-05-18T04:30:09.914Z" },
- { url = "https://files.pythonhosted.org/packages/7c/8a/3076c496ca8dafe0e8cd03fcebdfc47be4b1174b4e5b24ff6e396e6b3af2/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:054dc20fd2e3132b4c3883b4a00d72fd6e1f56fdaf89fccd12e8057d74cd74d7", size = 453181, upload-time = "2026-05-18T04:30:14.829Z" },
- { url = "https://files.pythonhosted.org/packages/e5/10/9745e17c98e7b8a86454df0a3c7b5686bd650383f1e9f26e4ebcbd6cc0c0/watchfiles-1.2.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:e140ed30ebde76796b686e67c182cff10ea2fbab186fafd1560f74bb5a473a6e", size = 465109, upload-time = "2026-05-18T04:30:28.123Z" },
- { url = "https://files.pythonhosted.org/packages/8f/95/8ef4a95481d3e0cb52d62a06fa6e972e81424be2d9698b91a2fecca9904c/watchfiles-1.2.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:bb7e52ecf68ba46d22df23467b87cffeb2146908aa523ebfe803019618cfda06", size = 630653, upload-time = "2026-05-18T04:31:49.304Z" },
- { url = "https://files.pythonhosted.org/packages/fd/e4/3b3bf36b0f829b50c6ebcb8d031583863c59f923d6a6af3d485e470d0fac/watchfiles-1.2.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:23282a321c8baf9b3a3c4afff673f9fe65eb7fdc2338d765ccad9d3d1916a5ba", size = 657838, upload-time = "2026-05-18T04:31:06.497Z" },
- { url = "https://files.pythonhosted.org/packages/21/b1/6cbbb50c1f3002ab568777d44aa21206dfb8807a840990c4037523b51812/watchfiles-1.2.0-cp314-cp314-win32.whl", hash = "sha256:c0db965c5f79aa49fe672d297cf1febc5ad149b658594944f49a54a2b96270a7", size = 275108, upload-time = "2026-05-18T04:30:06.891Z" },
- { url = "https://files.pythonhosted.org/packages/92/45/190ce6db8dcb4536682cf75d3889ff1a27182a58cb519d343cb6d9ea63d8/watchfiles-1.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:71283b39fd17e5408eb123bd37aeecfd9d54c81fc184421943208aadb879d103", size = 288441, upload-time = "2026-05-18T04:32:12.901Z" },
- { url = "https://files.pythonhosted.org/packages/74/0d/3eae1c2313ab08378431d907c3f8095ecca00f3eda33111cf4f0f2591799/watchfiles-1.2.0-cp314-cp314-win_arm64.whl", hash = "sha256:c5c19526f4e54a00f2666a6c0e9e40d582c09e865055ea7378bf0009aab857b3", size = 280684, upload-time = "2026-05-18T04:31:26.902Z" },
- { url = "https://files.pythonhosted.org/packages/b1/75/fb64e6c25d6b5ca636d03df34ffb1c6e9873303e76d27967e045f8df088f/watchfiles-1.2.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:d73a585accffa5ae39c17264c36ec3166d2fad7000c780f5ef83b2722afb9dd2", size = 398857, upload-time = "2026-05-18T04:32:17.108Z" },
- { url = "https://files.pythonhosted.org/packages/73/4e/9f7adf01754cbf81843722ccfec169d8f26c69778281a302855cecd2ee08/watchfiles-1.2.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ae99b14c5f21e026e0e9d96f40e07d8570ebee6cafd9d8fc318354606daa7a28", size = 392413, upload-time = "2026-05-18T04:31:07.911Z" },
- { url = "https://files.pythonhosted.org/packages/47/c8/bec626bcc2d69f44b9acb24ce7d60ed7b16b73628eea747fcbd169d8edda/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4429f3b105524a10b72c3a819b091c495d2811d419c1e1e8df773a5a5974f831", size = 452409, upload-time = "2026-05-18T04:31:20.142Z" },
- { url = "https://files.pythonhosted.org/packages/00/b7/b6362068e81e7c556d155a34c35d40ac3ef42d747b06d7f6e5bf58e359c2/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:43d818978d06062d9b22c4fab2ebe44cf5213d42dc8e62bda8c2760cfa2eeb33", size = 458827, upload-time = "2026-05-18T04:32:06.219Z" },
- { url = "https://files.pythonhosted.org/packages/67/f8/9a813fa42afb1e0b4625e75f0479826644d3ee8dc287e093799bc01f390c/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b9f732dc58b2dbe69e464ccf8fff7a03b0dd0be439da4c0720d3558527d3d6b4", size = 490104, upload-time = "2026-05-18T04:31:56.034Z" },
- { url = "https://files.pythonhosted.org/packages/2f/bf/27dfb6094ca4c9aad21298b5525b6c53cb36121ee454331d05161e58d130/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8f200104103feb097de4cab8fe4f5dd18a2026934c7dea98c55a2f5fd6d5a33b", size = 571360, upload-time = "2026-05-18T04:31:57.133Z" },
- { url = "https://files.pythonhosted.org/packages/fb/39/44a096d67270ea93df91d33877dbe91fbda3aa4f8ec2edf799d93eda8736/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:63ac26eefbf4af1741247d6fb68b11c49a25b2f7413fbd318a83a12aaa9cf666", size = 464644, upload-time = "2026-05-18T04:30:57.33Z" },
- { url = "https://files.pythonhosted.org/packages/0e/80/c7472203bad6268e3ef1ad260739704847898938ad7ea8b63a5131f46b50/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c4997d4e4a55f0d02b6cde327322daf3a0400e5df6c6b15948994bf72497925", size = 454771, upload-time = "2026-05-18T04:30:48.736Z" },
- { url = "https://files.pythonhosted.org/packages/51/cf/3b10b268b4b7f0fc26e9debb5eef1998b515887840f444cd3ec80c688755/watchfiles-1.2.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:4c887eba18b7945ac73067a8b4a66f21cd46c2539b2bc68588f7be6c7eb6d26b", size = 463494, upload-time = "2026-05-18T04:31:33.826Z" },
- { url = "https://files.pythonhosted.org/packages/3d/3e/a4302545cd589262a0dc7d140e86f7688eba3f9c72776c27f7e23b8864c4/watchfiles-1.2.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:3416ff151bb6b5a8d8d11664974fbef4d9305b9b2957839ab5a270468fd8df30", size = 629383, upload-time = "2026-05-18T04:31:15.596Z" },
- { url = "https://files.pythonhosted.org/packages/db/99/d5649df0a9a410d45b7c882304d0b790903ac9b6e8f2cfd12114e0c6b9f2/watchfiles-1.2.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:0e831a271c035d89789cffc386b6aa1375f39f1cd25eb7ca0997e4970d152fc5", size = 656093, upload-time = "2026-05-18T04:31:58.707Z" },
- { url = "https://files.pythonhosted.org/packages/92/b9/362702539275019a54dd2e94511b31a9b89c5f9e6a21966de7eb692549fc/watchfiles-1.2.0-cp315-cp315-macosx_10_12_x86_64.whl", hash = "sha256:37a6721cdf3f65dbb13aa9503510ccb4451603ac837e44d265d7992a597e1374", size = 400109, upload-time = "2026-05-18T04:31:16.879Z" },
- { url = "https://files.pythonhosted.org/packages/8f/75/71d5ba62db781e5587bded1d944c675374bc4aa37ff33d5018d98e8b6538/watchfiles-1.2.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:2b37d10b5a63bd4d87e18472d80fa525bd670586fae62e5dd580452764879b65", size = 392167, upload-time = "2026-05-18T04:31:28.058Z" },
- { url = "https://files.pythonhosted.org/packages/3c/01/c66dd95d0423fe30d31820e2d1d5bda773764131bbb6ac0cb1cf303ac328/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a105bc2283f67e8fbec74253ec2d94925de92ed72c0393f1206bf326b7b7b69", size = 452372, upload-time = "2026-05-18T04:31:00.836Z" },
- { url = "https://files.pythonhosted.org/packages/91/15/2fe99557e72f85627c6a8eed50d889e8d101623e060a22ad75b875cb932d/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5327989a465505f05cfe06f04fa9d0c2fd5432bb243e10e6f012b1bdca3c8579", size = 459596, upload-time = "2026-05-18T04:31:34.96Z" },
- { url = "https://files.pythonhosted.org/packages/ed/23/d4acfa0023367428ed48351b3b9b267893037b6cadae55620c61c24bcfd4/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ecb47f183a8025b2aa18b546725c3657e542112ae9c0613a2af79b4fa8d04ad7", size = 490869, upload-time = "2026-05-18T04:31:59.923Z" },
- { url = "https://files.pythonhosted.org/packages/a4/5f/3164cbdce06c9fb95c4f7b9e2f9760b5e2797af43a9ecc317ef42a23a278/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8520a4ab0e37f770afc34459c4f8f7019e153f9124dc101c15538365875d1ab2", size = 571641, upload-time = "2026-05-18T04:32:00.948Z" },
- { url = "https://files.pythonhosted.org/packages/41/e6/85d3731c55e65cd7690f3f803d24c139588aaf863e4bf2148fe7a7fa1a19/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:71cd71740ed2c15211ebb237ced4e39a1cdf6f80566e5fe95428da1626f4fde6", size = 464444, upload-time = "2026-05-18T04:30:34.298Z" },
- { url = "https://files.pythonhosted.org/packages/f4/7d/562641012b8b09872742c3b8adf9629ec479fd78f8d68ae4a0c13da8add6/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f88af53d6ddaf72179ef613ddc905e6f4785f712b49b80b3bef9f3525e6194b4", size = 453593, upload-time = "2026-05-18T04:31:23.464Z" },
- { url = "https://files.pythonhosted.org/packages/56/fe/cb8ef3d6f929d14158fdaaad9925985b7310abc9384dcd4d82dd0016fb59/watchfiles-1.2.0-cp315-cp315-manylinux_2_31_riscv64.whl", hash = "sha256:cee9d5efd929efdac5f7e58f72b3376f676b64050a91c5b99a7094c5b2317488", size = 465096, upload-time = "2026-05-18T04:31:30.384Z" },
- { url = "https://files.pythonhosted.org/packages/25/91/80908e835e100527a9267147b08c0eee1fa6ab0ffec15edc04d1d44885f7/watchfiles-1.2.0-cp315-cp315-musllinux_1_1_aarch64.whl", hash = "sha256:b718bf356bbc15e559bd8ef41782b573b8ae0e3f177ab244b440568d7ea02cfb", size = 630638, upload-time = "2026-05-18T04:30:49.89Z" },
- { url = "https://files.pythonhosted.org/packages/46/4b/95ab2f256bb4af3cb2eb23b9317bda984ee6e0f11733a5c004a6c95b06e3/watchfiles-1.2.0-cp315-cp315-musllinux_1_1_x86_64.whl", hash = "sha256:922c0e019fe68b3ae392965a766b02a71ba1168c932cebc3733cd52c5fe5b377", size = 657684, upload-time = "2026-05-18T04:31:32.027Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/4d/70a7feced9f87e2ff26dba42667290f41694fc64646c67261fbb8cab5d5c/watchfiles-1.2.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:01ea8d66f0693b9b60a6541c8d10263091ca9a9060d242f3c1f3143f9aad2c98", size = 399730, upload-time = "2026-05-18T04:31:38.162Z" },
+ { url = "https://files.pythonhosted.org/packages/31/3a/0da302f2307aee316922806ebd5726c542cbd787c938271cf14a074c7daf/watchfiles-1.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7ba0480b9a74af058f43b337e937a451e109295c420916d68ad24e3dc02f5e44", size = 392842, upload-time = "2026-05-18T04:30:27.051Z" },
+ { url = "https://files.pythonhosted.org/packages/db/ef/d5bdb705c224dbc256aa0c1ec47bf4e61ec52558f2afb44a71a1fe4d7015/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4f34e26a19f91f710c08e0183429f0d1d15df734e6bc78c31e77b9ea9c433658", size = 452989, upload-time = "2026-05-18T04:31:11.945Z" },
+ { url = "https://files.pythonhosted.org/packages/71/29/5495f2c1661949ef7a35e4d71111d129cfe7606414a26887a919d0a55406/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b4e77f6a55f858504069abd35d336a637555c09bca453dde1ee1e5ada8a6a1fb", size = 458978, upload-time = "2026-05-18T04:30:52.606Z" },
+ { url = "https://files.pythonhosted.org/packages/d5/8c/7f9c07c433811c2fffd93e13fdfb7135de9aab5f2ae41be08960fa0047dc/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0cb4d80e212f116474a545c21c912b445f16bb0cef9e6a73a498164223e14e2f", size = 490248, upload-time = "2026-05-18T04:31:36.003Z" },
+ { url = "https://files.pythonhosted.org/packages/3c/11/d93632febc52fbc21be90231bb7c17fd5387f46c9076fd40a5f9c2ae6910/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b974946a10af379d425e2eef5b62f5c6ebeaccf91d45eaad6f5b27ecd4f91aa0", size = 571847, upload-time = "2026-05-18T04:31:10.862Z" },
+ { url = "https://files.pythonhosted.org/packages/55/b4/383173e73aabb07ad1d9c7aa859d95437ac46a6d6a1e11005facda0c9d19/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:86bc13c25a8d1fcd70b51d0ce7c9b65e90de5666fcbfd3e34957cc73ee19aeb5", size = 465974, upload-time = "2026-05-18T04:30:17.006Z" },
+ { url = "https://files.pythonhosted.org/packages/a7/6c/89b1a230a78f57c52dd8893adb1f92f94411721b6ec12596c56d98c74356/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca148d73dea36c9763aaa351e4d7a51780ec1584217c45276f4fe8239c768b71", size = 454782, upload-time = "2026-05-18T04:30:35.656Z" },
+ { url = "https://files.pythonhosted.org/packages/24/62/1732118367cfff0a9fce3bf62ff4bfded09ef5df21d9d446b858b3f70a96/watchfiles-1.2.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:c525543d91961c6955b2636b308569e84a1d1c5f5f2932041ab9ef46422f43e3", size = 465182, upload-time = "2026-05-18T04:30:20.846Z" },
+ { url = "https://files.pythonhosted.org/packages/28/96/716f7e5f51339bf22963f3345f9f27d7f3b30e2eadc597e257c881dd3c53/watchfiles-1.2.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:a204794696ffb8f9b10fba6f7cb5216d42f3b2b71860ccac6b6e42f5f10973b0", size = 629841, upload-time = "2026-05-18T04:31:05.397Z" },
+ { url = "https://files.pythonhosted.org/packages/4c/fe/c40783950fd771ccf66ab3ec2722d188a9af1c7f96c6e811f36e40c6e03f/watchfiles-1.2.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:10d86db20695afe7997ac9e1717637d6714a8d0220458c33f3d2061f54cec427", size = 658028, upload-time = "2026-05-18T04:31:48.22Z" },
+ { url = "https://files.pythonhosted.org/packages/71/72/4508db1856d1d87fcbb3b63f4839bab1b5682cb0e8d224d122263c09654a/watchfiles-1.2.0-cp313-cp313-win32.whl", hash = "sha256:eb283ee99e21ad6443c8cdb06ac5b34b1308c329cbdf03fa02b445363714c799", size = 275183, upload-time = "2026-05-18T04:30:59.57Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/36/14b76ca57652e5cc5fd1c11f32a261292c08a0d19a00351013c2549cbfb2/watchfiles-1.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:a0f27f01bee51861392bb6b7c4fdb290b27d1eb194e9e28788d68102a0e898d9", size = 288059, upload-time = "2026-05-18T04:32:07.937Z" },
+ { url = "https://files.pythonhosted.org/packages/1b/8d/0a85e395398d8d20fadfe5c5d32c726eee17a519e78fb356f2cf7531bffe/watchfiles-1.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:3651aa7058595e9cfb75d35dd5ada2bf9f48a5b8a0f3562821d3e210c507e077", size = 280186, upload-time = "2026-05-18T04:31:54.484Z" },
+ { url = "https://files.pythonhosted.org/packages/37/68/36db056f1fdcc5f07302f56e631774d6835bcd6fa3ace402304621d5f9e5/watchfiles-1.2.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:faea288b6f0ab1902ef08f4ca6de005dccf856c4e0c4f21b8c5fce02d90a1b08", size = 399031, upload-time = "2026-05-18T04:30:44.576Z" },
+ { url = "https://files.pythonhosted.org/packages/c1/64/01a9d6f66a82a5c101ce939274106cc72759d62427e153f01edd2b9f87c2/watchfiles-1.2.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:01859b11fd9fbca670f4d5da00fbac282cfea9bd67a2125d8b2833a3b5617ea9", size = 391205, upload-time = "2026-05-18T04:30:25.413Z" },
+ { url = "https://files.pythonhosted.org/packages/84/2c/0a44fe058cb4bb7b8ede6b6670698bbb7c0400740e378d00022189b7b31d/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fff610d7bb2256a317bb1e96f0d7862c7aa8076733ee5df0fd41bbe76a24a4f4", size = 451892, upload-time = "2026-05-18T04:32:14.005Z" },
+ { url = "https://files.pythonhosted.org/packages/67/a1/351e0d56cd35e6488b5c8b4fb11a809a5bc923e8fe8fed9faf8920be0c89/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b141a4891c995a039cd89e9a49e62df1dc8a559a5d1a6e4c7106d16c12777a55", size = 458867, upload-time = "2026-05-18T04:31:22.279Z" },
+ { url = "https://files.pythonhosted.org/packages/d5/7d/9d09605187f1b838998624049fcf8bf47b73c1a3b76901fcac1782f62277/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f22943b7770483f6ea0721c6b11d022947a98eb0acae14694de034f4d0d38925", size = 490217, upload-time = "2026-05-18T04:31:43.657Z" },
+ { url = "https://files.pythonhosted.org/packages/60/5d/a17a16eccb182f04188cd308ec24b1a71a9b5c4e7098269cf35d9fa56d02/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1bc6195825b7dcd217968bb1f801a60fd4c16e8eeab5bedc7fe917d7d5995ab4", size = 571458, upload-time = "2026-05-18T04:32:11.875Z" },
+ { url = "https://files.pythonhosted.org/packages/d3/3d/4dd457062083ab1938e5dfd45032eb425cee2ac817287ca8ff4356183e5d/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d4a4b147f5dca2a5d325a06a832fb43f345751adfbc63204aec30e0d9ca965a2", size = 464707, upload-time = "2026-05-18T04:30:43.492Z" },
+ { url = "https://files.pythonhosted.org/packages/c6/71/ea8c57b128f5383de74d0c7d2d9c57ad7c9a65a930c451bd25d524b295b7/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4543579a9bdb0c9560039b4ffddbdb39545707659fbc430ce4c10f3f68d557f9", size = 454663, upload-time = "2026-05-18T04:30:16.061Z" },
+ { url = "https://files.pythonhosted.org/packages/53/fd/2e812bf938406d7db351f0703ddd3fc6c061cf30d96153a77bc79a943a44/watchfiles-1.2.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:20aa0e708b920bde876a4aa82dc7dd6ebea228a63a67cda6632c2fc87b787efa", size = 463537, upload-time = "2026-05-18T04:31:44.9Z" },
+ { url = "https://files.pythonhosted.org/packages/86/56/d17a7f1dd1bc3035f1072694a551301272f1739c2d8e319c927cb9e29b38/watchfiles-1.2.0-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:d413349d565dab74297f2a63e84a097936be69bf8f3b3801f27f380e32040f44", size = 629194, upload-time = "2026-05-18T04:31:14.141Z" },
+ { url = "https://files.pythonhosted.org/packages/be/06/f1ff66bf5cae50aa4062779a0ecd0bbaf15e466195719074078947d9a17d/watchfiles-1.2.0-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:f28b2725eb8cce327b9b3ab02415c853011dc55c95832fe90de6bc56f5315f72", size = 656194, upload-time = "2026-05-18T04:31:47.14Z" },
+ { url = "https://files.pythonhosted.org/packages/e7/54/a9c7ea9a82a4ac65e7004c0a03920b5cdd2f9c3b678757d9cd425aa51d53/watchfiles-1.2.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:b8c8358484d5fa12ef34f05b7f4168eaf1932f408725ff6d023c33ec17bd79d4", size = 400205, upload-time = "2026-05-18T04:32:05.153Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/5d/c9ab3534374a4a67450696905d6ef16a04405448b8dc52bd752ae50423d4/watchfiles-1.2.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9f04b092229ad2c50126dd3c922c8822e51e605993764a33058d4a791ab42281", size = 392508, upload-time = "2026-05-18T04:30:54.849Z" },
+ { url = "https://files.pythonhosted.org/packages/26/ca/1ad30103535cf0cecd7b993e8d50edc5351b1820e38f2d22e3df58962feb/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7a7ce236284f002a156f70add88efe5c70879cccbb658be0822c54b1306fc09d", size = 452448, upload-time = "2026-05-18T04:30:53.727Z" },
+ { url = "https://files.pythonhosted.org/packages/37/a1/ceee2cdf2afbd715fa07758d39c9859513eae411b23196f7fd039e5feedd/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b9909cc2b48468b575eefa944919e1fe8a36c5849d5c7c168f80a8c1db69398e", size = 459605, upload-time = "2026-05-18T04:30:23.312Z" },
+ { url = "https://files.pythonhosted.org/packages/e8/f6/421e30fd1cb3907a84ed92ab3f1983e37ba2dca015e9a894a048418417a2/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a37faaed405c67e28e6be45a1fa4f206ef5a2860f27c237db9fa30704c38242", size = 490757, upload-time = "2026-05-18T04:30:47.358Z" },
+ { url = "https://files.pythonhosted.org/packages/41/b0/55ed1b97ed08be7bba6f9a541cac15f2a858e1d74d2b07b6da70a82aab00/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9649193aa27bd9ff2e80ff29bfaa93085496c7a3a377592823cc58b77ee88add", size = 568672, upload-time = "2026-05-18T04:30:38.915Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/cf/d8ae8a80dd7bafab395ea7681c10237311bbf34d37704a8c744e7cf31fc7/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e4ff8e37f99cf1da89e255e07c9c4b37c214038c4283707bdec308cb1b0ea1f", size = 464197, upload-time = "2026-05-18T04:30:09.914Z" },
+ { url = "https://files.pythonhosted.org/packages/7c/8a/3076c496ca8dafe0e8cd03fcebdfc47be4b1174b4e5b24ff6e396e6b3af2/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:054dc20fd2e3132b4c3883b4a00d72fd6e1f56fdaf89fccd12e8057d74cd74d7", size = 453181, upload-time = "2026-05-18T04:30:14.829Z" },
+ { url = "https://files.pythonhosted.org/packages/e5/10/9745e17c98e7b8a86454df0a3c7b5686bd650383f1e9f26e4ebcbd6cc0c0/watchfiles-1.2.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:e140ed30ebde76796b686e67c182cff10ea2fbab186fafd1560f74bb5a473a6e", size = 465109, upload-time = "2026-05-18T04:30:28.123Z" },
+ { url = "https://files.pythonhosted.org/packages/8f/95/8ef4a95481d3e0cb52d62a06fa6e972e81424be2d9698b91a2fecca9904c/watchfiles-1.2.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:bb7e52ecf68ba46d22df23467b87cffeb2146908aa523ebfe803019618cfda06", size = 630653, upload-time = "2026-05-18T04:31:49.304Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/e4/3b3bf36b0f829b50c6ebcb8d031583863c59f923d6a6af3d485e470d0fac/watchfiles-1.2.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:23282a321c8baf9b3a3c4afff673f9fe65eb7fdc2338d765ccad9d3d1916a5ba", size = 657838, upload-time = "2026-05-18T04:31:06.497Z" },
+ { url = "https://files.pythonhosted.org/packages/21/b1/6cbbb50c1f3002ab568777d44aa21206dfb8807a840990c4037523b51812/watchfiles-1.2.0-cp314-cp314-win32.whl", hash = "sha256:c0db965c5f79aa49fe672d297cf1febc5ad149b658594944f49a54a2b96270a7", size = 275108, upload-time = "2026-05-18T04:30:06.891Z" },
+ { url = "https://files.pythonhosted.org/packages/92/45/190ce6db8dcb4536682cf75d3889ff1a27182a58cb519d343cb6d9ea63d8/watchfiles-1.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:71283b39fd17e5408eb123bd37aeecfd9d54c81fc184421943208aadb879d103", size = 288441, upload-time = "2026-05-18T04:32:12.901Z" },
+ { url = "https://files.pythonhosted.org/packages/74/0d/3eae1c2313ab08378431d907c3f8095ecca00f3eda33111cf4f0f2591799/watchfiles-1.2.0-cp314-cp314-win_arm64.whl", hash = "sha256:c5c19526f4e54a00f2666a6c0e9e40d582c09e865055ea7378bf0009aab857b3", size = 280684, upload-time = "2026-05-18T04:31:26.902Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/75/fb64e6c25d6b5ca636d03df34ffb1c6e9873303e76d27967e045f8df088f/watchfiles-1.2.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:d73a585accffa5ae39c17264c36ec3166d2fad7000c780f5ef83b2722afb9dd2", size = 398857, upload-time = "2026-05-18T04:32:17.108Z" },
+ { url = "https://files.pythonhosted.org/packages/73/4e/9f7adf01754cbf81843722ccfec169d8f26c69778281a302855cecd2ee08/watchfiles-1.2.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ae99b14c5f21e026e0e9d96f40e07d8570ebee6cafd9d8fc318354606daa7a28", size = 392413, upload-time = "2026-05-18T04:31:07.911Z" },
+ { url = "https://files.pythonhosted.org/packages/47/c8/bec626bcc2d69f44b9acb24ce7d60ed7b16b73628eea747fcbd169d8edda/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4429f3b105524a10b72c3a819b091c495d2811d419c1e1e8df773a5a5974f831", size = 452409, upload-time = "2026-05-18T04:31:20.142Z" },
+ { url = "https://files.pythonhosted.org/packages/00/b7/b6362068e81e7c556d155a34c35d40ac3ef42d747b06d7f6e5bf58e359c2/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:43d818978d06062d9b22c4fab2ebe44cf5213d42dc8e62bda8c2760cfa2eeb33", size = 458827, upload-time = "2026-05-18T04:32:06.219Z" },
+ { url = "https://files.pythonhosted.org/packages/67/f8/9a813fa42afb1e0b4625e75f0479826644d3ee8dc287e093799bc01f390c/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b9f732dc58b2dbe69e464ccf8fff7a03b0dd0be439da4c0720d3558527d3d6b4", size = 490104, upload-time = "2026-05-18T04:31:56.034Z" },
+ { url = "https://files.pythonhosted.org/packages/2f/bf/27dfb6094ca4c9aad21298b5525b6c53cb36121ee454331d05161e58d130/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8f200104103feb097de4cab8fe4f5dd18a2026934c7dea98c55a2f5fd6d5a33b", size = 571360, upload-time = "2026-05-18T04:31:57.133Z" },
+ { url = "https://files.pythonhosted.org/packages/fb/39/44a096d67270ea93df91d33877dbe91fbda3aa4f8ec2edf799d93eda8736/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:63ac26eefbf4af1741247d6fb68b11c49a25b2f7413fbd318a83a12aaa9cf666", size = 464644, upload-time = "2026-05-18T04:30:57.33Z" },
+ { url = "https://files.pythonhosted.org/packages/0e/80/c7472203bad6268e3ef1ad260739704847898938ad7ea8b63a5131f46b50/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c4997d4e4a55f0d02b6cde327322daf3a0400e5df6c6b15948994bf72497925", size = 454771, upload-time = "2026-05-18T04:30:48.736Z" },
+ { url = "https://files.pythonhosted.org/packages/51/cf/3b10b268b4b7f0fc26e9debb5eef1998b515887840f444cd3ec80c688755/watchfiles-1.2.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:4c887eba18b7945ac73067a8b4a66f21cd46c2539b2bc68588f7be6c7eb6d26b", size = 463494, upload-time = "2026-05-18T04:31:33.826Z" },
+ { url = "https://files.pythonhosted.org/packages/3d/3e/a4302545cd589262a0dc7d140e86f7688eba3f9c72776c27f7e23b8864c4/watchfiles-1.2.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:3416ff151bb6b5a8d8d11664974fbef4d9305b9b2957839ab5a270468fd8df30", size = 629383, upload-time = "2026-05-18T04:31:15.596Z" },
+ { url = "https://files.pythonhosted.org/packages/db/99/d5649df0a9a410d45b7c882304d0b790903ac9b6e8f2cfd12114e0c6b9f2/watchfiles-1.2.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:0e831a271c035d89789cffc386b6aa1375f39f1cd25eb7ca0997e4970d152fc5", size = 656093, upload-time = "2026-05-18T04:31:58.707Z" },
+ { url = "https://files.pythonhosted.org/packages/92/b9/362702539275019a54dd2e94511b31a9b89c5f9e6a21966de7eb692549fc/watchfiles-1.2.0-cp315-cp315-macosx_10_12_x86_64.whl", hash = "sha256:37a6721cdf3f65dbb13aa9503510ccb4451603ac837e44d265d7992a597e1374", size = 400109, upload-time = "2026-05-18T04:31:16.879Z" },
+ { url = "https://files.pythonhosted.org/packages/8f/75/71d5ba62db781e5587bded1d944c675374bc4aa37ff33d5018d98e8b6538/watchfiles-1.2.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:2b37d10b5a63bd4d87e18472d80fa525bd670586fae62e5dd580452764879b65", size = 392167, upload-time = "2026-05-18T04:31:28.058Z" },
+ { url = "https://files.pythonhosted.org/packages/3c/01/c66dd95d0423fe30d31820e2d1d5bda773764131bbb6ac0cb1cf303ac328/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a105bc2283f67e8fbec74253ec2d94925de92ed72c0393f1206bf326b7b7b69", size = 452372, upload-time = "2026-05-18T04:31:00.836Z" },
+ { url = "https://files.pythonhosted.org/packages/91/15/2fe99557e72f85627c6a8eed50d889e8d101623e060a22ad75b875cb932d/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5327989a465505f05cfe06f04fa9d0c2fd5432bb243e10e6f012b1bdca3c8579", size = 459596, upload-time = "2026-05-18T04:31:34.96Z" },
+ { url = "https://files.pythonhosted.org/packages/ed/23/d4acfa0023367428ed48351b3b9b267893037b6cadae55620c61c24bcfd4/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ecb47f183a8025b2aa18b546725c3657e542112ae9c0613a2af79b4fa8d04ad7", size = 490869, upload-time = "2026-05-18T04:31:59.923Z" },
+ { url = "https://files.pythonhosted.org/packages/a4/5f/3164cbdce06c9fb95c4f7b9e2f9760b5e2797af43a9ecc317ef42a23a278/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8520a4ab0e37f770afc34459c4f8f7019e153f9124dc101c15538365875d1ab2", size = 571641, upload-time = "2026-05-18T04:32:00.948Z" },
+ { url = "https://files.pythonhosted.org/packages/41/e6/85d3731c55e65cd7690f3f803d24c139588aaf863e4bf2148fe7a7fa1a19/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:71cd71740ed2c15211ebb237ced4e39a1cdf6f80566e5fe95428da1626f4fde6", size = 464444, upload-time = "2026-05-18T04:30:34.298Z" },
+ { url = "https://files.pythonhosted.org/packages/f4/7d/562641012b8b09872742c3b8adf9629ec479fd78f8d68ae4a0c13da8add6/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f88af53d6ddaf72179ef613ddc905e6f4785f712b49b80b3bef9f3525e6194b4", size = 453593, upload-time = "2026-05-18T04:31:23.464Z" },
+ { url = "https://files.pythonhosted.org/packages/56/fe/cb8ef3d6f929d14158fdaaad9925985b7310abc9384dcd4d82dd0016fb59/watchfiles-1.2.0-cp315-cp315-manylinux_2_31_riscv64.whl", hash = "sha256:cee9d5efd929efdac5f7e58f72b3376f676b64050a91c5b99a7094c5b2317488", size = 465096, upload-time = "2026-05-18T04:31:30.384Z" },
+ { url = "https://files.pythonhosted.org/packages/25/91/80908e835e100527a9267147b08c0eee1fa6ab0ffec15edc04d1d44885f7/watchfiles-1.2.0-cp315-cp315-musllinux_1_1_aarch64.whl", hash = "sha256:b718bf356bbc15e559bd8ef41782b573b8ae0e3f177ab244b440568d7ea02cfb", size = 630638, upload-time = "2026-05-18T04:30:49.89Z" },
+ { url = "https://files.pythonhosted.org/packages/46/4b/95ab2f256bb4af3cb2eb23b9317bda984ee6e0f11733a5c004a6c95b06e3/watchfiles-1.2.0-cp315-cp315-musllinux_1_1_x86_64.whl", hash = "sha256:922c0e019fe68b3ae392965a766b02a71ba1168c932cebc3733cd52c5fe5b377", size = 657684, upload-time = "2026-05-18T04:31:32.027Z" },
]
[[package]]
@@ -1759,32 +1760,32 @@ version = "16.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/04/24/4b2031d72e840ce4c1ccb255f693b15c334757fc50023e4db9537080b8c4/websockets-16.0.tar.gz", hash = "sha256:5f6261a5e56e8d5c42a4497b364ea24d94d9563e8fbd44e78ac40879c60179b5", size = 179346, upload-time = "2026-01-10T09:23:47.181Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/cc/9c/baa8456050d1c1b08dd0ec7346026668cbc6f145ab4e314d707bb845bf0d/websockets-16.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:878b336ac47938b474c8f982ac2f7266a540adc3fa4ad74ae96fea9823a02cc9", size = 177364, upload-time = "2026-01-10T09:22:59.333Z" },
- { url = "https://files.pythonhosted.org/packages/7e/0c/8811fc53e9bcff68fe7de2bcbe75116a8d959ac699a3200f4847a8925210/websockets-16.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:52a0fec0e6c8d9a784c2c78276a48a2bdf099e4ccc2a4cad53b27718dbfd0230", size = 175039, upload-time = "2026-01-10T09:23:01.171Z" },
- { url = "https://files.pythonhosted.org/packages/aa/82/39a5f910cb99ec0b59e482971238c845af9220d3ab9fa76dd9162cda9d62/websockets-16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e6578ed5b6981005df1860a56e3617f14a6c307e6a71b4fff8c48fdc50f3ed2c", size = 175323, upload-time = "2026-01-10T09:23:02.341Z" },
- { url = "https://files.pythonhosted.org/packages/bd/28/0a25ee5342eb5d5f297d992a77e56892ecb65e7854c7898fb7d35e9b33bd/websockets-16.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:95724e638f0f9c350bb1c2b0a7ad0e83d9cc0c9259f3ea94e40d7b02a2179ae5", size = 184975, upload-time = "2026-01-10T09:23:03.756Z" },
- { url = "https://files.pythonhosted.org/packages/f9/66/27ea52741752f5107c2e41fda05e8395a682a1e11c4e592a809a90c6a506/websockets-16.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c0204dc62a89dc9d50d682412c10b3542d748260d743500a85c13cd1ee4bde82", size = 186203, upload-time = "2026-01-10T09:23:05.01Z" },
- { url = "https://files.pythonhosted.org/packages/37/e5/8e32857371406a757816a2b471939d51c463509be73fa538216ea52b792a/websockets-16.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:52ac480f44d32970d66763115edea932f1c5b1312de36df06d6b219f6741eed8", size = 185653, upload-time = "2026-01-10T09:23:06.301Z" },
- { url = "https://files.pythonhosted.org/packages/9b/67/f926bac29882894669368dc73f4da900fcdf47955d0a0185d60103df5737/websockets-16.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6e5a82b677f8f6f59e8dfc34ec06ca6b5b48bc4fcda346acd093694cc2c24d8f", size = 184920, upload-time = "2026-01-10T09:23:07.492Z" },
- { url = "https://files.pythonhosted.org/packages/3c/a1/3d6ccdcd125b0a42a311bcd15a7f705d688f73b2a22d8cf1c0875d35d34a/websockets-16.0-cp313-cp313-win32.whl", hash = "sha256:abf050a199613f64c886ea10f38b47770a65154dc37181bfaff70c160f45315a", size = 178255, upload-time = "2026-01-10T09:23:09.245Z" },
- { url = "https://files.pythonhosted.org/packages/6b/ae/90366304d7c2ce80f9b826096a9e9048b4bb760e44d3b873bb272cba696b/websockets-16.0-cp313-cp313-win_amd64.whl", hash = "sha256:3425ac5cf448801335d6fdc7ae1eb22072055417a96cc6b31b3861f455fbc156", size = 178689, upload-time = "2026-01-10T09:23:10.483Z" },
- { url = "https://files.pythonhosted.org/packages/f3/1d/e88022630271f5bd349ed82417136281931e558d628dd52c4d8621b4a0b2/websockets-16.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8cc451a50f2aee53042ac52d2d053d08bf89bcb31ae799cb4487587661c038a0", size = 177406, upload-time = "2026-01-10T09:23:12.178Z" },
- { url = "https://files.pythonhosted.org/packages/f2/78/e63be1bf0724eeb4616efb1ae1c9044f7c3953b7957799abb5915bffd38e/websockets-16.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:daa3b6ff70a9241cf6c7fc9e949d41232d9d7d26fd3522b1ad2b4d62487e9904", size = 175085, upload-time = "2026-01-10T09:23:13.511Z" },
- { url = "https://files.pythonhosted.org/packages/bb/f4/d3c9220d818ee955ae390cf319a7c7a467beceb24f05ee7aaaa2414345ba/websockets-16.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:fd3cb4adb94a2a6e2b7c0d8d05cb94e6f1c81a0cf9dc2694fb65c7e8d94c42e4", size = 175328, upload-time = "2026-01-10T09:23:14.727Z" },
- { url = "https://files.pythonhosted.org/packages/63/bc/d3e208028de777087e6fb2b122051a6ff7bbcca0d6df9d9c2bf1dd869ae9/websockets-16.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:781caf5e8eee67f663126490c2f96f40906594cb86b408a703630f95550a8c3e", size = 185044, upload-time = "2026-01-10T09:23:15.939Z" },
- { url = "https://files.pythonhosted.org/packages/ad/6e/9a0927ac24bd33a0a9af834d89e0abc7cfd8e13bed17a86407a66773cc0e/websockets-16.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:caab51a72c51973ca21fa8a18bd8165e1a0183f1ac7066a182ff27107b71e1a4", size = 186279, upload-time = "2026-01-10T09:23:17.148Z" },
- { url = "https://files.pythonhosted.org/packages/b9/ca/bf1c68440d7a868180e11be653c85959502efd3a709323230314fda6e0b3/websockets-16.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:19c4dc84098e523fd63711e563077d39e90ec6702aff4b5d9e344a60cb3c0cb1", size = 185711, upload-time = "2026-01-10T09:23:18.372Z" },
- { url = "https://files.pythonhosted.org/packages/c4/f8/fdc34643a989561f217bb477cbc47a3a07212cbda91c0e4389c43c296ebf/websockets-16.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a5e18a238a2b2249c9a9235466b90e96ae4795672598a58772dd806edc7ac6d3", size = 184982, upload-time = "2026-01-10T09:23:19.652Z" },
- { url = "https://files.pythonhosted.org/packages/dd/d1/574fa27e233764dbac9c52730d63fcf2823b16f0856b3329fc6268d6ae4f/websockets-16.0-cp314-cp314-win32.whl", hash = "sha256:a069d734c4a043182729edd3e9f247c3b2a4035415a9172fd0f1b71658a320a8", size = 177915, upload-time = "2026-01-10T09:23:21.458Z" },
- { url = "https://files.pythonhosted.org/packages/8a/f1/ae6b937bf3126b5134ce1f482365fde31a357c784ac51852978768b5eff4/websockets-16.0-cp314-cp314-win_amd64.whl", hash = "sha256:c0ee0e63f23914732c6d7e0cce24915c48f3f1512ec1d079ed01fc629dab269d", size = 178381, upload-time = "2026-01-10T09:23:22.715Z" },
- { url = "https://files.pythonhosted.org/packages/06/9b/f791d1db48403e1f0a27577a6beb37afae94254a8c6f08be4a23e4930bc0/websockets-16.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:a35539cacc3febb22b8f4d4a99cc79b104226a756aa7400adc722e83b0d03244", size = 177737, upload-time = "2026-01-10T09:23:24.523Z" },
- { url = "https://files.pythonhosted.org/packages/bd/40/53ad02341fa33b3ce489023f635367a4ac98b73570102ad2cdd770dacc9a/websockets-16.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:b784ca5de850f4ce93ec85d3269d24d4c82f22b7212023c974c401d4980ebc5e", size = 175268, upload-time = "2026-01-10T09:23:25.781Z" },
- { url = "https://files.pythonhosted.org/packages/74/9b/6158d4e459b984f949dcbbb0c5d270154c7618e11c01029b9bbd1bb4c4f9/websockets-16.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:569d01a4e7fba956c5ae4fc988f0d4e187900f5497ce46339c996dbf24f17641", size = 175486, upload-time = "2026-01-10T09:23:27.033Z" },
- { url = "https://files.pythonhosted.org/packages/e5/2d/7583b30208b639c8090206f95073646c2c9ffd66f44df967981a64f849ad/websockets-16.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:50f23cdd8343b984957e4077839841146f67a3d31ab0d00e6b824e74c5b2f6e8", size = 185331, upload-time = "2026-01-10T09:23:28.259Z" },
- { url = "https://files.pythonhosted.org/packages/45/b0/cce3784eb519b7b5ad680d14b9673a31ab8dcb7aad8b64d81709d2430aa8/websockets-16.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:152284a83a00c59b759697b7f9e9cddf4e3c7861dd0d964b472b70f78f89e80e", size = 186501, upload-time = "2026-01-10T09:23:29.449Z" },
- { url = "https://files.pythonhosted.org/packages/19/60/b8ebe4c7e89fb5f6cdf080623c9d92789a53636950f7abacfc33fe2b3135/websockets-16.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bc59589ab64b0022385f429b94697348a6a234e8ce22544e3681b2e9331b5944", size = 186062, upload-time = "2026-01-10T09:23:31.368Z" },
- { url = "https://files.pythonhosted.org/packages/88/a8/a080593f89b0138b6cba1b28f8df5673b5506f72879322288b031337c0b8/websockets-16.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:32da954ffa2814258030e5a57bc73a3635463238e797c7375dc8091327434206", size = 185356, upload-time = "2026-01-10T09:23:32.627Z" },
- { url = "https://files.pythonhosted.org/packages/c2/b6/b9afed2afadddaf5ebb2afa801abf4b0868f42f8539bfe4b071b5266c9fe/websockets-16.0-cp314-cp314t-win32.whl", hash = "sha256:5a4b4cc550cb665dd8a47f868c8d04c8230f857363ad3c9caf7a0c3bf8c61ca6", size = 178085, upload-time = "2026-01-10T09:23:33.816Z" },
- { url = "https://files.pythonhosted.org/packages/9f/3e/28135a24e384493fa804216b79a6a6759a38cc4ff59118787b9fb693df93/websockets-16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b14dc141ed6d2dde437cddb216004bcac6a1df0935d79656387bd41632ba0bbd", size = 178531, upload-time = "2026-01-10T09:23:35.016Z" },
- { url = "https://files.pythonhosted.org/packages/6f/28/258ebab549c2bf3e64d2b0217b973467394a9cea8c42f70418ca2c5d0d2e/websockets-16.0-py3-none-any.whl", hash = "sha256:1637db62fad1dc833276dded54215f2c7fa46912301a24bd94d45d46a011ceec", size = 171598, upload-time = "2026-01-10T09:23:45.395Z" },
+ { url = "https://files.pythonhosted.org/packages/cc/9c/baa8456050d1c1b08dd0ec7346026668cbc6f145ab4e314d707bb845bf0d/websockets-16.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:878b336ac47938b474c8f982ac2f7266a540adc3fa4ad74ae96fea9823a02cc9", size = 177364, upload-time = "2026-01-10T09:22:59.333Z" },
+ { url = "https://files.pythonhosted.org/packages/7e/0c/8811fc53e9bcff68fe7de2bcbe75116a8d959ac699a3200f4847a8925210/websockets-16.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:52a0fec0e6c8d9a784c2c78276a48a2bdf099e4ccc2a4cad53b27718dbfd0230", size = 175039, upload-time = "2026-01-10T09:23:01.171Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/82/39a5f910cb99ec0b59e482971238c845af9220d3ab9fa76dd9162cda9d62/websockets-16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e6578ed5b6981005df1860a56e3617f14a6c307e6a71b4fff8c48fdc50f3ed2c", size = 175323, upload-time = "2026-01-10T09:23:02.341Z" },
+ { url = "https://files.pythonhosted.org/packages/bd/28/0a25ee5342eb5d5f297d992a77e56892ecb65e7854c7898fb7d35e9b33bd/websockets-16.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:95724e638f0f9c350bb1c2b0a7ad0e83d9cc0c9259f3ea94e40d7b02a2179ae5", size = 184975, upload-time = "2026-01-10T09:23:03.756Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/66/27ea52741752f5107c2e41fda05e8395a682a1e11c4e592a809a90c6a506/websockets-16.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c0204dc62a89dc9d50d682412c10b3542d748260d743500a85c13cd1ee4bde82", size = 186203, upload-time = "2026-01-10T09:23:05.01Z" },
+ { url = "https://files.pythonhosted.org/packages/37/e5/8e32857371406a757816a2b471939d51c463509be73fa538216ea52b792a/websockets-16.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:52ac480f44d32970d66763115edea932f1c5b1312de36df06d6b219f6741eed8", size = 185653, upload-time = "2026-01-10T09:23:06.301Z" },
+ { url = "https://files.pythonhosted.org/packages/9b/67/f926bac29882894669368dc73f4da900fcdf47955d0a0185d60103df5737/websockets-16.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6e5a82b677f8f6f59e8dfc34ec06ca6b5b48bc4fcda346acd093694cc2c24d8f", size = 184920, upload-time = "2026-01-10T09:23:07.492Z" },
+ { url = "https://files.pythonhosted.org/packages/3c/a1/3d6ccdcd125b0a42a311bcd15a7f705d688f73b2a22d8cf1c0875d35d34a/websockets-16.0-cp313-cp313-win32.whl", hash = "sha256:abf050a199613f64c886ea10f38b47770a65154dc37181bfaff70c160f45315a", size = 178255, upload-time = "2026-01-10T09:23:09.245Z" },
+ { url = "https://files.pythonhosted.org/packages/6b/ae/90366304d7c2ce80f9b826096a9e9048b4bb760e44d3b873bb272cba696b/websockets-16.0-cp313-cp313-win_amd64.whl", hash = "sha256:3425ac5cf448801335d6fdc7ae1eb22072055417a96cc6b31b3861f455fbc156", size = 178689, upload-time = "2026-01-10T09:23:10.483Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/1d/e88022630271f5bd349ed82417136281931e558d628dd52c4d8621b4a0b2/websockets-16.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8cc451a50f2aee53042ac52d2d053d08bf89bcb31ae799cb4487587661c038a0", size = 177406, upload-time = "2026-01-10T09:23:12.178Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/78/e63be1bf0724eeb4616efb1ae1c9044f7c3953b7957799abb5915bffd38e/websockets-16.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:daa3b6ff70a9241cf6c7fc9e949d41232d9d7d26fd3522b1ad2b4d62487e9904", size = 175085, upload-time = "2026-01-10T09:23:13.511Z" },
+ { url = "https://files.pythonhosted.org/packages/bb/f4/d3c9220d818ee955ae390cf319a7c7a467beceb24f05ee7aaaa2414345ba/websockets-16.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:fd3cb4adb94a2a6e2b7c0d8d05cb94e6f1c81a0cf9dc2694fb65c7e8d94c42e4", size = 175328, upload-time = "2026-01-10T09:23:14.727Z" },
+ { url = "https://files.pythonhosted.org/packages/63/bc/d3e208028de777087e6fb2b122051a6ff7bbcca0d6df9d9c2bf1dd869ae9/websockets-16.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:781caf5e8eee67f663126490c2f96f40906594cb86b408a703630f95550a8c3e", size = 185044, upload-time = "2026-01-10T09:23:15.939Z" },
+ { url = "https://files.pythonhosted.org/packages/ad/6e/9a0927ac24bd33a0a9af834d89e0abc7cfd8e13bed17a86407a66773cc0e/websockets-16.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:caab51a72c51973ca21fa8a18bd8165e1a0183f1ac7066a182ff27107b71e1a4", size = 186279, upload-time = "2026-01-10T09:23:17.148Z" },
+ { url = "https://files.pythonhosted.org/packages/b9/ca/bf1c68440d7a868180e11be653c85959502efd3a709323230314fda6e0b3/websockets-16.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:19c4dc84098e523fd63711e563077d39e90ec6702aff4b5d9e344a60cb3c0cb1", size = 185711, upload-time = "2026-01-10T09:23:18.372Z" },
+ { url = "https://files.pythonhosted.org/packages/c4/f8/fdc34643a989561f217bb477cbc47a3a07212cbda91c0e4389c43c296ebf/websockets-16.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a5e18a238a2b2249c9a9235466b90e96ae4795672598a58772dd806edc7ac6d3", size = 184982, upload-time = "2026-01-10T09:23:19.652Z" },
+ { url = "https://files.pythonhosted.org/packages/dd/d1/574fa27e233764dbac9c52730d63fcf2823b16f0856b3329fc6268d6ae4f/websockets-16.0-cp314-cp314-win32.whl", hash = "sha256:a069d734c4a043182729edd3e9f247c3b2a4035415a9172fd0f1b71658a320a8", size = 177915, upload-time = "2026-01-10T09:23:21.458Z" },
+ { url = "https://files.pythonhosted.org/packages/8a/f1/ae6b937bf3126b5134ce1f482365fde31a357c784ac51852978768b5eff4/websockets-16.0-cp314-cp314-win_amd64.whl", hash = "sha256:c0ee0e63f23914732c6d7e0cce24915c48f3f1512ec1d079ed01fc629dab269d", size = 178381, upload-time = "2026-01-10T09:23:22.715Z" },
+ { url = "https://files.pythonhosted.org/packages/06/9b/f791d1db48403e1f0a27577a6beb37afae94254a8c6f08be4a23e4930bc0/websockets-16.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:a35539cacc3febb22b8f4d4a99cc79b104226a756aa7400adc722e83b0d03244", size = 177737, upload-time = "2026-01-10T09:23:24.523Z" },
+ { url = "https://files.pythonhosted.org/packages/bd/40/53ad02341fa33b3ce489023f635367a4ac98b73570102ad2cdd770dacc9a/websockets-16.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:b784ca5de850f4ce93ec85d3269d24d4c82f22b7212023c974c401d4980ebc5e", size = 175268, upload-time = "2026-01-10T09:23:25.781Z" },
+ { url = "https://files.pythonhosted.org/packages/74/9b/6158d4e459b984f949dcbbb0c5d270154c7618e11c01029b9bbd1bb4c4f9/websockets-16.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:569d01a4e7fba956c5ae4fc988f0d4e187900f5497ce46339c996dbf24f17641", size = 175486, upload-time = "2026-01-10T09:23:27.033Z" },
+ { url = "https://files.pythonhosted.org/packages/e5/2d/7583b30208b639c8090206f95073646c2c9ffd66f44df967981a64f849ad/websockets-16.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:50f23cdd8343b984957e4077839841146f67a3d31ab0d00e6b824e74c5b2f6e8", size = 185331, upload-time = "2026-01-10T09:23:28.259Z" },
+ { url = "https://files.pythonhosted.org/packages/45/b0/cce3784eb519b7b5ad680d14b9673a31ab8dcb7aad8b64d81709d2430aa8/websockets-16.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:152284a83a00c59b759697b7f9e9cddf4e3c7861dd0d964b472b70f78f89e80e", size = 186501, upload-time = "2026-01-10T09:23:29.449Z" },
+ { url = "https://files.pythonhosted.org/packages/19/60/b8ebe4c7e89fb5f6cdf080623c9d92789a53636950f7abacfc33fe2b3135/websockets-16.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bc59589ab64b0022385f429b94697348a6a234e8ce22544e3681b2e9331b5944", size = 186062, upload-time = "2026-01-10T09:23:31.368Z" },
+ { url = "https://files.pythonhosted.org/packages/88/a8/a080593f89b0138b6cba1b28f8df5673b5506f72879322288b031337c0b8/websockets-16.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:32da954ffa2814258030e5a57bc73a3635463238e797c7375dc8091327434206", size = 185356, upload-time = "2026-01-10T09:23:32.627Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/b6/b9afed2afadddaf5ebb2afa801abf4b0868f42f8539bfe4b071b5266c9fe/websockets-16.0-cp314-cp314t-win32.whl", hash = "sha256:5a4b4cc550cb665dd8a47f868c8d04c8230f857363ad3c9caf7a0c3bf8c61ca6", size = 178085, upload-time = "2026-01-10T09:23:33.816Z" },
+ { url = "https://files.pythonhosted.org/packages/9f/3e/28135a24e384493fa804216b79a6a6759a38cc4ff59118787b9fb693df93/websockets-16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b14dc141ed6d2dde437cddb216004bcac6a1df0935d79656387bd41632ba0bbd", size = 178531, upload-time = "2026-01-10T09:23:35.016Z" },
+ { url = "https://files.pythonhosted.org/packages/6f/28/258ebab549c2bf3e64d2b0217b973467394a9cea8c42f70418ca2c5d0d2e/websockets-16.0-py3-none-any.whl", hash = "sha256:1637db62fad1dc833276dded54215f2c7fa46912301a24bd94d45d46a011ceec", size = 171598, upload-time = "2026-01-10T09:23:45.395Z" },
]
From 2334a4b6e32599c05988d9bccf4a70b7a3074b13 Mon Sep 17 00:00:00 2001
From: Andrey Avtomonov
Date: Tue, 2 Jun 2026 20:03:27 +0200
Subject: [PATCH 07/49] Emit ingest_completed once per target on every ingest
path
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
emitIngestCompleted was called only in runKtxPublicIngest's plain/json loop,
so the foreground 'ktx ingest' view and all of 'ktx setup' — which delegate to
runContextBuild -> executePublicIngestTarget — never emitted the event. That
left ingest_completed near-useless for measuring ingestion.
Move the emit into executePublicIngestTarget, the single per-target chokepoint
every entrypoint funnels through: a thin wrapper now captures timing, runs the
existing steps (extracted to runIngestTargetSteps), and emits exactly once. The
telemetry echo targets deps.runtimeIo (the real user stream) so a capture
buffer used for step output doesn't swallow it. Thread project through the
context-build call site. No schema/field changes, so Node<->Python telemetry
parity is unaffected.
Add tests: the shared chokepoint emits exactly one ingest_completed for any
caller, and a multi-target run emits one per target with no double-emit.
---
packages/cli/src/context-build-view.ts | 2 +-
packages/cli/src/public-ingest.ts | 32 ++++++++---
packages/cli/test/context-build-view.test.ts | 2 +
packages/cli/test/public-ingest.test.ts | 58 ++++++++++++++++++++
4 files changed, 86 insertions(+), 8 deletions(-)
diff --git a/packages/cli/src/context-build-view.ts b/packages/cli/src/context-build-view.ts
index f088097d..0ddd4922 100644
--- a/packages/cli/src/context-build-view.ts
+++ b/packages/cli/src/context-build-view.ts
@@ -997,7 +997,7 @@ export async function runContextBuild(
let result: KtxPublicIngestTargetResult | null = null;
let thrownError: unknown = null;
try {
- result = await execTarget(targetState.target, runArgs, capture.io, progressDeps);
+ result = await execTarget(targetState.target, runArgs, capture.io, progressDeps, project);
} catch (error) {
thrownError = error;
}
diff --git a/packages/cli/src/public-ingest.ts b/packages/cli/src/public-ingest.ts
index 216d1d7b..7fc43ac4 100644
--- a/packages/cli/src/public-ingest.ts
+++ b/packages/cli/src/public-ingest.ts
@@ -862,11 +862,34 @@ function capturedFailureMessage(output: string): string | undefined {
return [firstLine, ...followupLines].join('\n');
}
+/**
+ * Run one ingest target through its scan/ingest steps. The single per-target
+ * chokepoint reached by every entrypoint — standalone `ktx ingest` (plain/json
+ * and foreground) and `ktx setup` (via `runContextBuild`). The exported
+ * `executePublicIngestTarget` wraps this and emits the `ingest_completed`
+ * telemetry event exactly once, so every path is counted.
+ */
export async function executePublicIngestTarget(
target: KtxPublicIngestPlanTarget,
args: Extract,
io: KtxCliIo,
deps: KtxPublicIngestDeps,
+ project: KtxPublicIngestProject,
+): Promise {
+ const startedAt = performance.now();
+ const result = await runIngestTargetSteps(target, args, io, deps);
+ // `io` may be a capture buffer for the scan/ingest step output; the telemetry
+ // debug echo belongs on the real user-facing stream, which callers expose as
+ // `deps.runtimeIo` (falling back to `io` when the step io is already real).
+ await emitIngestCompleted({ args, project, target, result, startedAt, io: deps.runtimeIo ?? io });
+ return result;
+}
+
+async function runIngestTargetSteps(
+ target: KtxPublicIngestPlanTarget,
+ args: Extract,
+ io: KtxCliIo,
+ deps: KtxPublicIngestDeps,
): Promise {
if (target.preflightFailure) {
if (target.operation === 'database-ingest') {
@@ -1086,11 +1109,8 @@ export async function runKtxPublicIngest(
}
for (const [index, target] of plan.targets.entries()) {
- const startedAt = performance.now();
if (args.json) {
- const result = await executePublicIngestTarget(target, args, io, deps);
- results.push(result);
- await emitIngestCompleted({ args, project, target, result, startedAt, io });
+ results.push(await executePublicIngestTarget(target, args, io, deps, project));
continue;
}
@@ -1108,9 +1128,7 @@ export async function runKtxPublicIngest(
onPhaseEnd: progress.onPhaseEnd,
runtimeIo: deps.runtimeIo ?? io,
};
- const result = await executePublicIngestTarget(target, args, capture, targetDeps);
- results.push(result);
- await emitIngestCompleted({ args, project, target, result, startedAt, io });
+ results.push(await executePublicIngestTarget(target, args, capture, targetDeps, project));
}
if (args.json) {
diff --git a/packages/cli/test/context-build-view.test.ts b/packages/cli/test/context-build-view.test.ts
index 40e33606..d8692eb5 100644
--- a/packages/cli/test/context-build-view.test.ts
+++ b/packages/cli/test/context-build-view.test.ts
@@ -984,6 +984,7 @@ describe('runContextBuild', () => {
scanProgress: expect.anything(),
ingestProgress: expect.any(Function),
}),
+ project,
);
});
@@ -1015,6 +1016,7 @@ describe('runContextBuild', () => {
expect.objectContaining({
runtimeIo: io.io,
}),
+ project,
);
});
diff --git a/packages/cli/test/public-ingest.test.ts b/packages/cli/test/public-ingest.test.ts
index 549756eb..2c27593e 100644
--- a/packages/cli/test/public-ingest.test.ts
+++ b/packages/cli/test/public-ingest.test.ts
@@ -6,11 +6,17 @@ import { initKtxProject } from '../src/context/project/project.js';
import { afterEach, describe, expect, it, vi } from 'vitest';
import {
buildPublicIngestPlan,
+ executePublicIngestTarget,
type KtxPublicIngestDeps,
type KtxPublicIngestProject,
publicProgressMessage,
runKtxPublicIngest,
} from '../src/public-ingest.js';
+
+/** Count non-overlapping occurrences of `needle` in `haystack`. */
+function occurrences(haystack: string, needle: string): number {
+ return haystack.split(needle).length - 1;
+}
import type { ManagedPythonCommandRuntime } from '../src/managed-python-command.js';
function makeIo(options: { isTTY?: boolean; interactive?: boolean } = {}) {
@@ -457,6 +463,58 @@ describe('runKtxPublicIngest', () => {
}
});
+ it('emits exactly one ingest_completed from the shared executePublicIngestTarget chokepoint', async () => {
+ // executePublicIngestTarget is the single per-target path reached by every
+ // entrypoint (plain/json ingest, foreground ingest via runContextBuild, and
+ // setup). Emitting here is what makes ingest_completed fire on every path.
+ vi.stubEnv('KTX_TELEMETRY_DEBUG', '1');
+ vi.stubEnv('CI', '');
+ const io = makeIo({ isTTY: true });
+ const project = deepReadyProject({ warehouse: { driver: 'postgres' } });
+ const [target] = buildPublicIngestPlan(project, {
+ projectDir: '/tmp/project',
+ targetConnectionId: 'warehouse',
+ all: false,
+ }).targets;
+
+ const result = await executePublicIngestTarget(
+ target,
+ { command: 'run', projectDir: '/tmp/project', targetConnectionId: 'warehouse', all: false, json: false, inputMode: 'disabled' },
+ io.io,
+ { runScan: vi.fn(async () => 0) },
+ project,
+ );
+
+ expect(result.steps.some((step) => step.status === 'failed')).toBe(false);
+ expect(occurrences(io.stderr(), '"event":"ingest_completed"')).toBe(1);
+ expect(io.stderr()).toContain('"outcome":"ok"');
+ });
+
+ it('emits one ingest_completed per target and never double-emits across a multi-target run', async () => {
+ vi.stubEnv('KTX_TELEMETRY_DEBUG', '1');
+ vi.stubEnv('CI', '');
+ const projectDir = await mkdtemp(join(tmpdir(), 'ktx-public-ingest-no-double-'));
+ try {
+ await initKtxProject({ projectDir });
+ const io = makeIo({ isTTY: true });
+ const project = deepReadyProject({
+ first: { driver: 'sqlite', path: join(projectDir, 'first.sqlite') },
+ second: { driver: 'sqlite', path: join(projectDir, 'second.sqlite') },
+ });
+
+ const code = await runKtxPublicIngest(
+ { command: 'run', projectDir, all: true, json: false, inputMode: 'disabled' },
+ io.io,
+ { loadProject: vi.fn(async () => project), runScan: vi.fn(async () => 0) },
+ );
+
+ expect(code).toBe(0);
+ expect(occurrences(io.stderr(), '"event":"ingest_completed"')).toBe(2);
+ } finally {
+ await rm(projectDir, { recursive: true, force: true });
+ }
+ });
+
it('runs query history after schema ingest with current-run window override', async () => {
const io = makeIo();
const runtimeIo = makeIo({ isTTY: true });
From cb6a67c2d7df2b6272cab1235812d15466feadb5 Mon Sep 17 00:00:00 2001
From: Andrey Avtomonov
Date: Tue, 2 Jun 2026 23:19:37 +0200
Subject: [PATCH 08/49] Make telemetry reliable across interrupts and headless
installs
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Three reliability gaps surfaced while auditing why PostHog numbers were
untrustworthy:
1. Interrupted commands lost their events. capture() is fire-and-forget and the
only flush guarantee lived in a finally block, which SIGINT/SIGTERM skip — so
Ctrl-C'ing a long ingest or an MCP client killing 'ktx mcp stdio' dropped the
command event and any queued events. Add SIGINT/SIGTERM handlers (real-process
entry only; never under test/programmatic io) that mark the active command
span aborted, emit it, drain the emitter, then exit. Idempotent with the
normal finally path via the single-consume command span.
2. Headless-first installs were invisible. loadTelemetryIdentity refused to mint
an installId unless stdout was a TTY, so a machine whose first run was an
IDE-launched MCP server or a script emitted nothing, ever. Mint on first run
regardless of surface (still honoring CI/DO_NOT_TRACK/KTX_TELEMETRY_DISABLED),
writing the one-time notice to stderr — safe under the MCP stdio protocol,
which reserves stdout. Drop the now-unused stdoutIsTTY option.
3. No guard against silent emit regressions (the 0.7.0 scan_completed blackout).
Add tests: the shared executePublicIngestTarget chokepoint emits exactly one
ingest_completed on success and on the preflight-failure branch, and a
database target invokes the scan that emits scan_completed; plus coverage for
the aborted-flush helper.
Identity is unchanged otherwise: every event still attributes to the installId
in ~/.ktx/telemetry.json. No event/field changes, so Node<->Python schema parity
is untouched. Docs updated to reflect first-run-on-any-surface activation.
---
.../content/docs/community/telemetry.mdx | 9 +-
packages/cli/src/cli-runtime.ts | 53 ++++++++++-
packages/cli/src/telemetry/identity.ts | 14 ++-
packages/cli/src/telemetry/index.ts | 19 +++-
packages/cli/test/public-ingest.test.ts | 36 ++++++-
packages/cli/test/telemetry/identity.test.ts | 93 ++++++++++---------
packages/cli/test/telemetry/index.test.ts | 61 +++++++++++-
7 files changed, 219 insertions(+), 66 deletions(-)
diff --git a/docs-site/content/docs/community/telemetry.mdx b/docs-site/content/docs/community/telemetry.mdx
index 9618af8c..a3a10564 100644
--- a/docs-site/content/docs/community/telemetry.mdx
+++ b/docs-site/content/docs/community/telemetry.mdx
@@ -6,11 +6,10 @@ description: Understand what usage telemetry ktx collects and how to opt out.
**ktx** collects aggregated usage telemetry so maintainers can see
which commands work, where setup fails, and which parts of the data-agent
workflow need improvement. Telemetry is opt-out: it turns on the first time you
-run **ktx** in an interactive terminal, which prints a one-time notice. From
-then on the same install also reports background activity that has no terminal
-of its own, such as the local MCP server your agent calls. It stays disabled in
-CI, whenever an opt-out is set, and until that first interactive run has shown
-the notice.
+run **ktx** in any way — an interactive command, a script, or an
+agent-launched MCP server — and prints a one-time notice (to the terminal when
+there is one, otherwise to standard error). It stays disabled in CI and whenever
+an opt-out is set.
## Opt out
diff --git a/packages/cli/src/cli-runtime.ts b/packages/cli/src/cli-runtime.ts
index 68089720..7043143b 100644
--- a/packages/cli/src/cli-runtime.ts
+++ b/packages/cli/src/cli-runtime.ts
@@ -89,6 +89,46 @@ export async function runInitForCommander(
return await runInit(args, io);
}
+function signalExitCode(signal: NodeJS.Signals): number {
+ // 128 + signal number: SIGINT (2) -> 130, SIGTERM (15) -> 143.
+ return signal === 'SIGTERM' ? 143 : 130;
+}
+
+/**
+ * Flush telemetry on interrupt for the real CLI process. `capture()` is
+ * fire-and-forget and the only flush guarantee lives in a `finally` a signal
+ * skips, so Ctrl-C / `kill` of a long-running command (ingest, `mcp stdio`)
+ * would otherwise drop its `command` event and queued events. Installed only
+ * when driving the actual process; programmatic/test callers pass their own
+ * `io` and never reach here. Returns a disposer that removes the listeners.
+ */
+function installTelemetrySignalFlush(io: KtxCliIo, info: KtxCliPackageInfo): () => void {
+ let handling = false;
+ const handle = (signal: NodeJS.Signals): void => {
+ if (handling) {
+ process.exit(signalExitCode(signal));
+ }
+ handling = true;
+ void (async () => {
+ try {
+ const { emitAbortedCommandAndShutdown } = await import('./telemetry/index.js');
+ await emitAbortedCommandAndShutdown({ packageInfo: info, io });
+ } catch {
+ // Best-effort: never let a telemetry hiccup block the interrupt exit.
+ }
+ process.exit(signalExitCode(signal));
+ })();
+ };
+ const onSigint = (): void => handle('SIGINT');
+ const onSigterm = (): void => handle('SIGTERM');
+ process.on('SIGINT', onSigint);
+ process.on('SIGTERM', onSigterm);
+ return () => {
+ process.off('SIGINT', onSigint);
+ process.off('SIGTERM', onSigterm);
+ };
+}
+
export async function runKtxCli(
argv = process.argv.slice(2),
io: KtxCliIo = process,
@@ -98,7 +138,14 @@ export async function runKtxCli(
profileMark('runtime:runKtxCli');
const { runCommanderKtxCli } = await profileSpan('import ./cli-program.js', () => import('./cli-program.js'));
- return await runCommanderKtxCli(argv, io, deps, info, {
- runInit: runInitForCommander,
- });
+ // Real-process entry only: flush telemetry if interrupted. Test/programmatic
+ // callers pass their own `io`, so they never install process-level handlers.
+ const removeSignalFlush = (io as unknown) === process ? installTelemetrySignalFlush(io, info) : undefined;
+ try {
+ return await runCommanderKtxCli(argv, io, deps, info, {
+ runInit: runInitForCommander,
+ });
+ } finally {
+ removeSignalFlush?.();
+ }
}
diff --git a/packages/cli/src/telemetry/identity.ts b/packages/cli/src/telemetry/identity.ts
index d699ea1f..ee5f7a39 100644
--- a/packages/cli/src/telemetry/identity.ts
+++ b/packages/cli/src/telemetry/identity.ts
@@ -37,7 +37,6 @@ function styleNotice(notice: string, env: TelemetryIdentityEnv): string {
export interface LoadTelemetryIdentityOptions {
homeDir?: string;
env?: TelemetryIdentityEnv;
- stdoutIsTTY: boolean;
stderr: { write(chunk: string): void };
now?: () => Date;
}
@@ -94,13 +93,12 @@ export async function loadTelemetryIdentity(options: LoadTelemetryIdentityOption
};
}
- // No identity yet. Minting one means showing the one-time opt-out notice, so
- // first-run creation requires an interactive surface; a headless first run
- // stays disabled and defers enablement until the next interactive run.
- if (options.stdoutIsTTY !== true) {
- return { enabled: false, createdFile: false, noticeShown: false, path };
- }
-
+ // No identity yet → mint one regardless of surface. Telemetry is opt-out, so
+ // a fresh install is counted even when its first run is headless (an
+ // IDE-launched `ktx mcp stdio`, a scripted invocation); otherwise those
+ // installs would be permanently invisible. Opt-out env vars are honored
+ // above. The one-time notice is written to stderr — safe even under MCP
+ // stdio, which reserves stdout for its JSON-RPC protocol.
const timestamp = (options.now ?? (() => new Date()))().toISOString();
const next = {
installId: randomUUID(),
diff --git a/packages/cli/src/telemetry/index.ts b/packages/cli/src/telemetry/index.ts
index c5b9b729..b02e0224 100644
--- a/packages/cli/src/telemetry/index.ts
+++ b/packages/cli/src/telemetry/index.ts
@@ -22,7 +22,6 @@ export type { CommandOutcome, CompletedCommandSpan };
export async function showTelemetryNoticeIfNeeded(io: KtxCliIo, packageInfo: KtxCliPackageInfo): Promise {
const identity = await loadTelemetryIdentity({
- stdoutIsTTY: io.stdout.isTTY === true,
stderr: io.stderr,
env: process.env,
});
@@ -81,7 +80,6 @@ export async function emitTelemetryEvent(input:
}): Promise {
const debug = telemetryDebugEnabled();
const identity = await loadTelemetryIdentity({
- stdoutIsTTY: input.io.stdout.isTTY === true,
stderr: input.io.stderr,
env: process.env,
});
@@ -154,3 +152,20 @@ export async function emitCompletedCommand(input: {
packageInfo: input.packageInfo,
});
}
+
+/**
+ * Flush telemetry when the process is interrupted (Ctrl-C / kill). The normal
+ * `command` emit + flush lives in a `finally` that a signal skips, so without
+ * this an interrupted long-running command (ingest, `mcp stdio`) loses its
+ * `command` event and any queued events. Marks the active command span as
+ * `aborted`, emits it, and drains the emitter. Best-effort and idempotent: if
+ * the span was already completed (normal exit racing a signal) the emit no-ops.
+ */
+export async function emitAbortedCommandAndShutdown(input: {
+ packageInfo: KtxCliPackageInfo;
+ io: KtxCliIo;
+}): Promise {
+ const completed = completeCommandSpan({ completedAt: performance.now(), outcome: 'aborted' });
+ await emitCompletedCommand({ completed, packageInfo: input.packageInfo, io: input.io });
+ await shutdownTelemetryEmitter();
+}
diff --git a/packages/cli/test/public-ingest.test.ts b/packages/cli/test/public-ingest.test.ts
index 2c27593e..1a8b457e 100644
--- a/packages/cli/test/public-ingest.test.ts
+++ b/packages/cli/test/public-ingest.test.ts
@@ -477,17 +477,51 @@ describe('runKtxPublicIngest', () => {
all: false,
}).targets;
+ const runScan = vi.fn(async () => 0);
const result = await executePublicIngestTarget(
target,
{ command: 'run', projectDir: '/tmp/project', targetConnectionId: 'warehouse', all: false, json: false, inputMode: 'disabled' },
io.io,
- { runScan: vi.fn(async () => 0) },
+ { runScan },
project,
);
expect(result.steps.some((step) => step.status === 'failed')).toBe(false);
expect(occurrences(io.stderr(), '"event":"ingest_completed"')).toBe(1);
expect(io.stderr()).toContain('"outcome":"ok"');
+ // A database-ingest target must run a scan — runKtxScan is what emits
+ // scan_completed, so this guards against the 0.7.0-style regression where a
+ // path stopped triggering the scan and the event silently went to zero.
+ expect(runScan).toHaveBeenCalledTimes(1);
+ });
+
+ it('still emits ingest_completed when a target fails preflight (early-return branch)', async () => {
+ // The chokepoint must emit on every internal branch, including the early
+ // preflight-failure return — otherwise failed-setup installs vanish.
+ vi.stubEnv('KTX_TELEMETRY_DEBUG', '1');
+ vi.stubEnv('CI', '');
+ const io = makeIo({ isTTY: true });
+ // projectWithConnections leaves enrichment unconfigured → preflight failure.
+ const project = projectWithConnections({ warehouse: { driver: 'postgres' } });
+ const [target] = buildPublicIngestPlan(project, {
+ projectDir: '/tmp/project',
+ targetConnectionId: 'warehouse',
+ all: false,
+ }).targets;
+ expect(target.preflightFailure).toBeTruthy();
+
+ const runScan = vi.fn(async () => 0);
+ await executePublicIngestTarget(
+ target,
+ { command: 'run', projectDir: '/tmp/project', targetConnectionId: 'warehouse', all: false, json: false, inputMode: 'disabled' },
+ io.io,
+ { runScan },
+ project,
+ );
+
+ expect(occurrences(io.stderr(), '"event":"ingest_completed"')).toBe(1);
+ expect(io.stderr()).toContain('"outcome":"error"');
+ expect(runScan).not.toHaveBeenCalled();
});
it('emits one ingest_completed per target and never double-emits across a multi-target run', async () => {
diff --git a/packages/cli/test/telemetry/identity.test.ts b/packages/cli/test/telemetry/identity.test.ts
index e5b6bddf..6c7e3f46 100644
--- a/packages/cli/test/telemetry/identity.test.ts
+++ b/packages/cli/test/telemetry/identity.test.ts
@@ -11,18 +11,15 @@ import {
type TelemetryIdentityEnv,
} from '../../src/telemetry/identity.js';
-function makeIo(stdoutIsTTY = true) {
+function makeIo() {
let stderr = '';
return {
- io: {
- stdout: { isTTY: stdoutIsTTY, write: () => {} },
- stderr: {
- write: (chunk: string) => {
- stderr += chunk;
- },
+ stderr: {
+ write: (chunk: string) => {
+ stderr += chunk;
},
},
- stderr: () => stderr,
+ read: () => stderr,
};
}
@@ -39,14 +36,13 @@ describe('telemetry identity', () => {
await rm(homeDir, { recursive: true, force: true });
});
- it('creates the telemetry file and one-line notice on first interactive enabled load', async () => {
- const testIo = makeIo(true);
+ it('creates the telemetry file and one-line notice on first enabled load', async () => {
+ const testIo = makeIo();
const identity = await loadTelemetryIdentity({
homeDir,
env,
- stdoutIsTTY: true,
- stderr: testIo.io.stderr,
+ stderr: testIo.stderr,
now: () => new Date('2026-05-22T14:33:02.000Z'),
});
@@ -54,7 +50,7 @@ describe('telemetry identity', () => {
expect(identity.installId).toMatch(/^[0-9a-f-]{36}$/);
expect(identity.createdFile).toBe(true);
expect(identity.noticeShown).toBe(true);
- expect(testIo.stderr()).toBe(`[2m${TELEMETRY_NOTICE}[22m\n`);
+ expect(testIo.read()).toBe(`\x1b[2m${TELEMETRY_NOTICE}\x1b[22m\n`);
const stored = JSON.parse(await readFile(join(homeDir, '.ktx', 'telemetry.json'), 'utf-8')) as {
enabled: boolean;
@@ -64,26 +60,46 @@ describe('telemetry identity', () => {
expect(stored.noticeShownVersion).toBe(1);
});
+ it('mints an identity on a headless first run (no TTY required)', async () => {
+ // A fresh install whose first invocation is headless (IDE-launched
+ // `ktx mcp stdio`, a scripted run) must still be counted. The one-time
+ // notice goes to stderr, which is safe even under the MCP stdio protocol.
+ const testIo = makeIo();
+
+ const identity = await loadTelemetryIdentity({
+ homeDir,
+ env,
+ stderr: testIo.stderr,
+ now: () => new Date('2026-05-22T14:33:02.000Z'),
+ });
+
+ expect(identity).toMatchObject({ enabled: true, createdFile: true, noticeShown: true });
+ expect(identity.installId).toMatch(/^[0-9a-f-]{36}$/);
+ expect(testIo.read()).toBe(`\x1b[2m${TELEMETRY_NOTICE}\x1b[22m\n`);
+ const stored = JSON.parse(await readFile(join(homeDir, '.ktx', 'telemetry.json'), 'utf-8')) as {
+ enabled: boolean;
+ };
+ expect(stored.enabled).toBe(true);
+ });
+
it('emits the notice without ANSI when NO_COLOR is set', async () => {
- const testIo = makeIo(true);
+ const testIo = makeIo();
await loadTelemetryIdentity({
homeDir,
env: { NO_COLOR: '1' },
- stdoutIsTTY: true,
- stderr: testIo.io.stderr,
+ stderr: testIo.stderr,
now: () => new Date('2026-05-22T14:33:02.000Z'),
});
- expect(testIo.stderr()).toBe(`${TELEMETRY_NOTICE}\n`);
+ expect(testIo.read()).toBe(`${TELEMETRY_NOTICE}\n`);
});
it('does not create a file when env disables telemetry', async () => {
const identity = await loadTelemetryIdentity({
homeDir,
env: { KTX_TELEMETRY_DISABLED: '1' },
- stdoutIsTTY: true,
- stderr: makeIo(true).io.stderr,
+ stderr: makeIo().stderr,
now: () => new Date('2026-05-22T14:33:02.000Z'),
});
@@ -91,26 +107,16 @@ describe('telemetry identity', () => {
await expect(readFile(join(homeDir, '.ktx', 'telemetry.json'), 'utf-8')).rejects.toThrow();
});
- it('does not create a file for CI or non-TTY command invocations', async () => {
+ it('does not create a file under CI', async () => {
await expect(
loadTelemetryIdentity({
homeDir,
env: { CI: '1' },
- stdoutIsTTY: true,
- stderr: makeIo(true).io.stderr,
- now: () => new Date('2026-05-22T14:33:02.000Z'),
- }),
- ).resolves.toMatchObject({ enabled: false, createdFile: false });
-
- await expect(
- loadTelemetryIdentity({
- homeDir,
- env: {},
- stdoutIsTTY: false,
- stderr: makeIo(false).io.stderr,
+ stderr: makeIo().stderr,
now: () => new Date('2026-05-22T14:33:02.000Z'),
}),
).resolves.toMatchObject({ enabled: false, createdFile: false });
+ await expect(readFile(join(homeDir, '.ktx', 'telemetry.json'), 'utf-8')).rejects.toThrow();
});
it('honors persistent enabled false', async () => {
@@ -135,8 +141,7 @@ describe('telemetry identity', () => {
loadTelemetryIdentity({
homeDir,
env,
- stdoutIsTTY: true,
- stderr: makeIo(true).io.stderr,
+ stderr: makeIo().stderr,
now: () => new Date('2026-05-22T15:00:00.000Z'),
}),
).resolves.toMatchObject({
@@ -146,7 +151,7 @@ describe('telemetry identity', () => {
});
});
- it('enables a consented identity without a TTY (MCP servers run headless)', async () => {
+ it('honors a consented identity without re-showing the notice', async () => {
await mkdir(join(homeDir, '.ktx'), { recursive: true });
await writeFile(
join(homeDir, '.ktx', 'telemetry.json'),
@@ -163,14 +168,13 @@ describe('telemetry identity', () => {
) + '\n',
'utf-8',
);
- const testIo = makeIo(false);
+ const testIo = makeIo();
await expect(
loadTelemetryIdentity({
homeDir,
env,
- stdoutIsTTY: false,
- stderr: testIo.io.stderr,
+ stderr: testIo.stderr,
now: () => new Date('2026-05-22T15:00:00.000Z'),
}),
).resolves.toMatchObject({
@@ -179,12 +183,11 @@ describe('telemetry identity', () => {
createdFile: false,
noticeShown: false,
});
- // The one-time notice belongs to interactive surfaces only; a headless load
- // must never write it (the MCP stdio protocol shares the process streams).
- expect(testIo.stderr()).toBe('');
+ // An already-consented identity must not re-emit the one-time notice.
+ expect(testIo.read()).toBe('');
});
- it('keeps opt-outs suppressing a consented identity without a TTY', async () => {
+ it('keeps opt-outs suppressing a consented identity', async () => {
await mkdir(join(homeDir, '.ktx'), { recursive: true });
await writeFile(
join(homeDir, '.ktx', 'telemetry.json'),
@@ -207,8 +210,7 @@ describe('telemetry identity', () => {
loadTelemetryIdentity({
homeDir,
env: optOut,
- stdoutIsTTY: false,
- stderr: makeIo(false).io.stderr,
+ stderr: makeIo().stderr,
now: () => new Date('2026-05-22T15:00:00.000Z'),
}),
).resolves.toMatchObject({ enabled: false });
@@ -222,8 +224,7 @@ describe('telemetry identity', () => {
const identity = await loadTelemetryIdentity({
homeDir,
env,
- stdoutIsTTY: true,
- stderr: makeIo(true).io.stderr,
+ stderr: makeIo().stderr,
now: () => new Date('2026-05-22T14:33:02.000Z'),
});
diff --git a/packages/cli/test/telemetry/index.test.ts b/packages/cli/test/telemetry/index.test.ts
index 8d8f932b..7e88410f 100644
--- a/packages/cli/test/telemetry/index.test.ts
+++ b/packages/cli/test/telemetry/index.test.ts
@@ -4,7 +4,8 @@ import { join } from 'node:path';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import type { KtxCliIo } from '../../src/cli-runtime.js';
-import { emitTelemetryEvent } from '../../src/telemetry/index.js';
+import { beginCommandSpan, emitAbortedCommandAndShutdown, emitTelemetryEvent } from '../../src/telemetry/index.js';
+import { resetCommandSpan } from '../../src/telemetry/command-hook.js';
function makeIo(): { io: KtxCliIo; stderr: () => string } {
let stderr = '';
@@ -61,3 +62,61 @@ describe('emitTelemetryEvent', () => {
await expect(readFile(join(homeDir, '.ktx', 'telemetry.json'), 'utf-8')).rejects.toThrow();
});
});
+
+describe('emitAbortedCommandAndShutdown', () => {
+ let homeDir: string;
+
+ beforeEach(async () => {
+ homeDir = await mkdtemp(join(tmpdir(), 'ktx-telemetry-abort-'));
+ vi.stubEnv('HOME', homeDir);
+ vi.stubEnv('KTX_TELEMETRY_DEBUG', '1');
+ vi.stubEnv('CI', '');
+ vi.stubEnv('KTX_TELEMETRY_DISABLED', '');
+ vi.stubEnv('DO_NOT_TRACK', '');
+ resetCommandSpan();
+ });
+
+ afterEach(async () => {
+ resetCommandSpan();
+ vi.unstubAllEnvs();
+ await rm(homeDir, { recursive: true, force: true });
+ });
+
+ it('flushes the active command span as aborted (the signal path)', async () => {
+ const testIo = makeIo();
+ beginCommandSpan({
+ commandPath: ['ktx', 'ingest'],
+ flagsPresent: {},
+ hasProject: true,
+ attachProjectGroup: false,
+ startedAt: performance.now(),
+ });
+
+ await emitAbortedCommandAndShutdown({
+ packageInfo: { name: '@kaelio/ktx', version: '0.0.0-test' },
+ io: testIo.io,
+ });
+
+ expect(testIo.stderr()).toContain('"event":"command"');
+ expect(testIo.stderr()).toContain('"outcome":"aborted"');
+ expect(testIo.stderr()).toContain('"commandPath":["ktx","ingest"]');
+ });
+
+ it('is idempotent: a second call (or no active span) emits nothing', async () => {
+ const testIo = makeIo();
+ beginCommandSpan({
+ commandPath: ['ktx', 'ingest'],
+ flagsPresent: {},
+ hasProject: true,
+ attachProjectGroup: false,
+ startedAt: performance.now(),
+ });
+ const pkg = { name: '@kaelio/ktx', version: '0.0.0-test' };
+
+ await emitAbortedCommandAndShutdown({ packageInfo: pkg, io: testIo.io });
+ const secondIo = makeIo();
+ await emitAbortedCommandAndShutdown({ packageInfo: pkg, io: secondIo.io });
+
+ expect(secondIo.stderr()).not.toContain('"event":"command"');
+ });
+});
From 45aa95d2cc121267bbbc8c184402a19573956dd4 Mon Sep 17 00:00:00 2001
From: Andrey Avtomonov
Date: Wed, 3 Jun 2026 01:00:21 +0200
Subject: [PATCH 09/49] feat(cli): guide next action at end of ktx setup, not
reruns (#256)
Re-running setup was the dominant action for installs that completed setup but never ingested. Classify completion (incomplete | needs-context | needs-agents | ready) and drive one obvious next action per state: route a config-complete project straight to the build, point unbuilt-context users at `ktx ingest` instead of re-running setup or dropping to a bare shell, and confirm readiness for fully-set-up projects rather than reopening the edit menu.
---
.../docs/getting-started/quickstart.mdx | 19 +-
packages/cli/src/next-steps.ts | 3 +-
packages/cli/src/setup-context.ts | 12 +-
packages/cli/src/setup-ready-menu.ts | 53 ++++-
packages/cli/src/setup.ts | 31 +--
packages/cli/test/next-steps.test.ts | 5 +-
packages/cli/test/setup-ready-menu.test.ts | 106 ++++++++--
packages/cli/test/setup.test.ts | 190 +++++++++++++++++-
8 files changed, 360 insertions(+), 59 deletions(-)
diff --git a/docs-site/content/docs/getting-started/quickstart.mdx b/docs-site/content/docs/getting-started/quickstart.mdx
index 5129c585..35ec6009 100644
--- a/docs-site/content/docs/getting-started/quickstart.mdx
+++ b/docs-site/content/docs/getting-started/quickstart.mdx
@@ -215,8 +215,8 @@ The wizard walks you through everything **ktx** needs in one pass:
SQLite, PostgreSQL, MySQL, SQL Server, BigQuery, and Snowflake.
5. **Context sources** - optionally adds dbt, MetricFlow, LookML, Looker,
Metabase, or Notion. You can skip and add them later.
-6. **Build** - runs the first ingest so semantic sources and wiki pages
- are ready for agents.
+6. **Build** - offers to run the first ingest so semantic sources and wiki
+ pages are ready for agents. If you skip it, build later with `ktx ingest`.
7. **Agent integration** - installs project-local rules for Claude Code,
Codex, Cursor, OpenCode, or universal `.agents`.
@@ -247,6 +247,18 @@ progress under `.ktx/setup/` and resumes from the remaining work.
> resuming setup, connecting an agent, checking status, or exploring a
> pre-built demo project.
+When the wizard finishes, it states where you stand and the single next action:
+
+- **Context built** - **ktx** confirms it is ready for agents and points you to
+ open your coding agent and ask a data question.
+- **Build skipped** - **ktx** tells you setup is complete and that the only step
+ left is to build context with `ktx ingest`.
+
+Re-running `ktx setup` on an already-configured project goes straight to the
+remaining step - building context or connecting an agent - instead of
+re-asking every question. Once everything is ready, it confirms you are set
+rather than reopening the configuration menu.
+
## Verify
When setup finishes, check readiness:
@@ -268,6 +280,9 @@ Agent integration ready: yes (codex:project)
For a structured check inside scripts, use `ktx status --json`.
+If you skipped the build, `ktx context built` shows `no`. Build it with
+`ktx ingest` - there is no need to re-run `ktx setup`.
+
When setup finishes building context, its final context check looks like:
```text
diff --git a/packages/cli/src/next-steps.ts b/packages/cli/src/next-steps.ts
index 80a1b441..b6726e3c 100644
--- a/packages/cli/src/next-steps.ts
+++ b/packages/cli/src/next-steps.ts
@@ -70,8 +70,7 @@ export function formatSetupNextStepLines(state: KtxSetupNextStepState, indent =
if (!state.contextReady) {
return [
- `${indent}Build KTX context next.`,
- `${indent}Run ingest to build database schema context before context-source ingest.`,
+ `${indent}Setup is complete. The only step left is to build context for your agents.`,
...commandLines(KTX_CONTEXT_BUILD_COMMANDS, indent),
];
}
diff --git a/packages/cli/src/setup-context.ts b/packages/cli/src/setup-context.ts
index d6ef2639..721c09bd 100644
--- a/packages/cli/src/setup-context.ts
+++ b/packages/cli/src/setup-context.ts
@@ -441,12 +441,10 @@ function writeMissingCapabilities(missing: string[], io: KtxCliIo): void {
io.stderr.write('\nFix this in setup before building context.\n');
}
-function writeSkippedContext(projectDir: string, io: KtxCliIo): void {
- io.stdout.write('\nKTX is configured, but context has not been built yet.\n\n');
- io.stdout.write('Agents were not connected because KTX has not prepared searchable context for them.\n\n');
- io.stdout.write(`Resume setup:\n ktx setup --project-dir ${resolve(projectDir)}\n\n`);
- io.stdout.write(`Build context:\n ktx setup --project-dir ${resolve(projectDir)}\n\n`);
- io.stdout.write(`Check status:\n ktx status --project-dir ${resolve(projectDir)}\n`);
+function writeSkippedContext(io: KtxCliIo): void {
+ // The setup completion screen owns "what to do next" (it points at `ktx ingest`),
+ // so keep this to a short acknowledgement rather than a competing command list.
+ io.stdout.write('\nLeaving context unbuilt for now.\n');
}
function writeSuccess(
@@ -695,7 +693,7 @@ export async function runKtxSetupContextStep(
return { status: 'back', projectDir: args.projectDir };
}
if (choice === 'skip') {
- writeSkippedContext(args.projectDir, io);
+ writeSkippedContext(io);
return { status: 'skipped', projectDir: args.projectDir };
}
}
diff --git a/packages/cli/src/setup-ready-menu.ts b/packages/cli/src/setup-ready-menu.ts
index f1f736e4..de0f5a45 100644
--- a/packages/cli/src/setup-ready-menu.ts
+++ b/packages/cli/src/setup-ready-menu.ts
@@ -14,6 +14,12 @@ export type KtxSetupReadyAction =
| 'agents'
| 'exit';
+/**
+ * Where a project stands once its `ktx.yaml` exists. Single source of truth for the
+ * end-of-setup interception: each state maps to exactly one obvious next action.
+ */
+export type KtxSetupCompletion = 'incomplete' | 'needs-context' | 'needs-agents' | 'ready';
+
interface KtxSetupReadyMenuPromptAdapter {
select(options: { message: string; options: KtxSetupPromptOption[] }): Promise;
cancel(message: string): void;
@@ -23,7 +29,11 @@ export interface KtxSetupReadyMenuDeps {
prompts?: KtxSetupReadyMenuPromptAdapter;
}
-export function isKtxPreAgentSetupReady(status: KtxSetupStatus): boolean {
+export function setupHasContextTargets(status: KtxSetupStatus): boolean {
+ return status.databases.length > 0 || status.sources.length > 0;
+}
+
+function setupConfigReady(status: KtxSetupStatus): boolean {
return (
status.project.ready &&
status.llm.ready &&
@@ -31,25 +41,58 @@ export function isKtxPreAgentSetupReady(status: KtxSetupStatus): boolean {
status.databases.every((database) => database.ready) &&
status.sources.every((source) => source.ready) &&
status.runtime.ready &&
- status.context.ready
+ setupHasContextTargets(status)
);
}
-export function isKtxSetupReady(status: KtxSetupStatus): boolean {
- return isKtxPreAgentSetupReady(status) && status.agents.some((agent) => agent.ready);
+export function classifyKtxSetupCompletion(status: KtxSetupStatus): KtxSetupCompletion {
+ if (!setupConfigReady(status)) {
+ return 'incomplete';
+ }
+ if (!status.context.ready) {
+ return 'needs-context';
+ }
+ if (!status.agents.some((agent) => agent.ready)) {
+ return 'needs-agents';
+ }
+ return 'ready';
}
function createPromptAdapter(): KtxSetupReadyMenuPromptAdapter {
return createKtxSetupPromptAdapter({ selectCancelValue: 'exit' });
}
+/**
+ * Shown when a returning user re-runs `ktx setup` on a fully-ready project. Leads with
+ * "you're done" (the readiness note is printed by the caller first) and keeps the
+ * section editor one explicit step away rather than defaulting into it.
+ */
+export async function runKtxSetupReadyMenu(
+ status: KtxSetupStatus,
+ deps: KtxSetupReadyMenuDeps = {},
+): Promise<{ action: KtxSetupReadyAction }> {
+ const prompts = deps.prompts ?? createPromptAdapter();
+ const choice = await prompts.select({
+ message: 'Anything else?',
+ options: [
+ { value: 'done', label: "Done — I'll start using ktx" },
+ { value: 'change', label: 'Change a setting' },
+ ],
+ });
+ if (choice !== 'change') {
+ return { action: 'exit' };
+ }
+ return runKtxSetupReadyChangeMenu(status, { prompts });
+}
+
+/** @internal Reached only through {@link runKtxSetupReadyMenu}; exported for unit tests. */
export async function runKtxSetupReadyChangeMenu(
status: KtxSetupStatus,
deps: KtxSetupReadyMenuDeps = {},
): Promise<{ action: KtxSetupReadyAction }> {
const prompts = deps.prompts ?? createPromptAdapter();
const action = (await prompts.select({
- message: `KTX is already set up for ${status.project.name ?? status.project.path}. What would you like to change?`,
+ message: 'What would you like to change?',
options: [
{ value: 'models', label: 'Models' },
{ value: 'embeddings', label: 'Embeddings' },
diff --git a/packages/cli/src/setup.ts b/packages/cli/src/setup.ts
index f8fc2064..7d4fdb0e 100644
--- a/packages/cli/src/setup.ts
+++ b/packages/cli/src/setup.ts
@@ -6,7 +6,7 @@ import { ktxLocalStateDbPath } from './context/project/local-state-db.js';
import { loadKtxProject, type KtxLocalProject } from './context/project/project.js';
import { readKtxSetupState } from './context/project/setup-config.js';
import { getKtxCliPackageInfo, type KtxCliIo } from './cli-runtime.js';
-import { formatSetupNextStepLines } from './next-steps.js';
+import { formatNextStepLines, formatSetupNextStepLines } from './next-steps.js';
import { runtimeInstallPolicyFromFlags } from './managed-python-command.js';
import { readManagedPythonRuntimeStatus } from './managed-python-runtime.js';
import { resolveProjectRuntimeRequirements } from './runtime-requirements.js';
@@ -33,10 +33,10 @@ import {
} from './setup-models.js';
import { type KtxSetupProjectDeps, runKtxSetupProjectStep } from './setup-project.js';
import {
- isKtxPreAgentSetupReady,
- isKtxSetupReady,
+ classifyKtxSetupCompletion,
type KtxSetupReadyMenuDeps,
- runKtxSetupReadyChangeMenu,
+ runKtxSetupReadyMenu,
+ setupHasContextTargets,
} from './setup-ready-menu.js';
import { type KtxSetupSourcesDeps, type KtxSetupSourceType, runKtxSetupSourcesStep } from './setup-sources.js';
import {
@@ -529,10 +529,6 @@ function setupStatusReady(status: KtxSetupStatus): boolean {
);
}
-function setupHasContextTargets(status: KtxSetupStatus): boolean {
- return status.databases.length > 0 || status.sources.length > 0;
-}
-
function setupContextReady(status: KtxSetupStatus): boolean {
return status.context.ready;
}
@@ -630,12 +626,19 @@ async function runKtxSetupInner(args: KtxSetupArgs, io: KtxCliIo, deps: KtxSetup
let readyAction: string | undefined;
if (args.inputMode !== 'disabled' && !agentsRequested) {
- if (isKtxSetupReady(currentStatus)) {
- readyAction = (await runKtxSetupReadyChangeMenu(currentStatus, deps.readyMenuDeps)).action;
- if (readyAction === 'exit') return 0;
- } else if (isKtxPreAgentSetupReady(currentStatus)) {
+ const completion = classifyKtxSetupCompletion(currentStatus);
+ if (completion === 'ready') {
+ setupUi.note(formatNextStepLines().join('\n'), 'ktx is ready', io);
+ const choice = (await runKtxSetupReadyMenu(currentStatus, deps.readyMenuDeps)).action;
+ if (choice === 'exit') return 0;
+ readyAction = choice;
+ } else if (completion === 'needs-context') {
+ // Config is done; skip the re-walk and land straight on the build prompt.
+ readyAction = 'context';
+ } else if (completion === 'needs-agents') {
readyAction = 'agents';
}
+ // 'incomplete' → readyAction stays undefined → run the full setup walk.
}
const runOnly = readyAction;
@@ -872,7 +875,9 @@ async function runKtxSetupInner(args: KtxSetupArgs, io: KtxCliIo, deps: KtxSetup
}
if (step === 'context' && stepResult.status !== 'ready') {
if (shouldRunAgents && args.skipAgents !== true) {
- return 0;
+ // Context isn't built, so skip agent install — but still reach the
+ // completion screen, which states readiness and points at `ktx ingest`.
+ break setupLoop;
}
}
diff --git a/packages/cli/test/next-steps.test.ts b/packages/cli/test/next-steps.test.ts
index c700de9e..eed0f3bf 100644
--- a/packages/cli/test/next-steps.test.ts
+++ b/packages/cli/test/next-steps.test.ts
@@ -65,8 +65,7 @@ describe('KTX demo next steps', () => {
agentIntegrationReady: true,
}).join('\n');
- expect(rendered).toContain('Build KTX context next.');
- expect(rendered).toContain('Run ingest to build database schema context before context-source ingest.');
+ expect(rendered).toContain('Setup is complete. The only step left is to build context for your agents.');
expect(rendered).toContain('ktx ingest');
expect(rendered).not.toContain('resume');
expect(rendered).not.toContain('scan');
@@ -87,6 +86,6 @@ describe('KTX demo next steps', () => {
expect(rendered).toContain('ktx status --json');
expect(rendered).not.toContain('ktx agent');
expect(rendered).not.toContain('ktx serve --mcp stdio --user-id local');
- expect(rendered).not.toContain('Build KTX context next.');
+ expect(rendered).not.toContain('Setup is complete.');
});
});
diff --git a/packages/cli/test/setup-ready-menu.test.ts b/packages/cli/test/setup-ready-menu.test.ts
index 82c92a1c..39c62a32 100644
--- a/packages/cli/test/setup-ready-menu.test.ts
+++ b/packages/cli/test/setup-ready-menu.test.ts
@@ -1,5 +1,9 @@
import { describe, expect, it, vi } from 'vitest';
-import { isKtxPreAgentSetupReady, isKtxSetupReady, runKtxSetupReadyChangeMenu } from '../src/setup-ready-menu.js';
+import {
+ classifyKtxSetupCompletion,
+ runKtxSetupReadyChangeMenu,
+ runKtxSetupReadyMenu,
+} from '../src/setup-ready-menu.js';
import type { KtxSetupStatus } from '../src/setup.js';
const readyStatus: KtxSetupStatus = {
@@ -13,32 +17,58 @@ const readyStatus: KtxSetupStatus = {
agents: [{ target: 'codex', scope: 'project', ready: true }],
};
-describe('setup ready menu', () => {
- it('recognizes a ready setup only when required sections are ready', () => {
- expect(isKtxSetupReady(readyStatus)).toBe(true);
- expect(isKtxSetupReady({ ...readyStatus, embeddings: { ready: false } })).toBe(false);
- expect(isKtxSetupReady({ ...readyStatus, runtime: { required: true, ready: false, features: ['core'] } })).toBe(false);
- expect(isKtxSetupReady({ ...readyStatus, context: { ready: false, status: 'not_started' } })).toBe(false);
- expect(isKtxSetupReady({ ...readyStatus, agents: [] })).toBe(false);
+describe('classifyKtxSetupCompletion', () => {
+ it('reports ready only when config, context, and agents are all ready', () => {
+ expect(classifyKtxSetupCompletion(readyStatus)).toBe('ready');
});
- it('recognizes pre-agent readiness without requiring agents', () => {
- expect(isKtxPreAgentSetupReady(readyStatus)).toBe(true);
- expect(isKtxPreAgentSetupReady({ ...readyStatus, agents: [] })).toBe(true);
- expect(isKtxPreAgentSetupReady({ ...readyStatus, embeddings: { ready: false } })).toBe(false);
- expect(isKtxPreAgentSetupReady({ ...readyStatus, runtime: { required: true, ready: false, features: ['core'] } })).toBe(
- false,
- );
- expect(isKtxPreAgentSetupReady({ ...readyStatus, context: { ready: false, status: 'not_started' } })).toBe(false);
+ it('reports needs-agents when config and context are ready but no agent is installed', () => {
+ expect(classifyKtxSetupCompletion({ ...readyStatus, agents: [] })).toBe('needs-agents');
});
- it('maps ready-project menu choices to setup sections', async () => {
- const prompts = { select: vi.fn(async () => 'agents'), cancel: vi.fn() };
+ it('reports needs-context when config is ready but context is not built', () => {
+ expect(
+ classifyKtxSetupCompletion({ ...readyStatus, context: { ready: false, status: 'not_started' } }),
+ ).toBe('needs-context');
+ });
- await expect(runKtxSetupReadyChangeMenu(readyStatus, { prompts })).resolves.toEqual({ action: 'agents' });
+ it('reports incomplete when a required config section is not ready', () => {
+ expect(classifyKtxSetupCompletion({ ...readyStatus, embeddings: { ready: false } })).toBe('incomplete');
+ expect(
+ classifyKtxSetupCompletion({ ...readyStatus, runtime: { required: true, ready: false, features: ['core'] } }),
+ ).toBe('incomplete');
+ });
+ it('reports incomplete when no context targets are configured', () => {
+ expect(classifyKtxSetupCompletion({ ...readyStatus, databases: [], sources: [] })).toBe('incomplete');
+ });
+});
+
+describe('runKtxSetupReadyMenu', () => {
+ it('exits when the user is done', async () => {
+ const prompts = { select: vi.fn(async () => 'done'), cancel: vi.fn() };
+
+ await expect(runKtxSetupReadyMenu(readyStatus, { prompts })).resolves.toEqual({ action: 'exit' });
+
+ expect(prompts.select).toHaveBeenCalledTimes(1);
expect(prompts.select).toHaveBeenCalledWith({
- message: 'KTX is already set up for /tmp/revenue. What would you like to change?',
+ message: 'Anything else?',
+ options: [
+ { value: 'done', label: "Done — I'll start using ktx" },
+ { value: 'change', label: 'Change a setting' },
+ ],
+ });
+ });
+
+ it('opens the section menu when the user chooses to change a setting', async () => {
+ const select = vi.fn().mockResolvedValueOnce('change').mockResolvedValueOnce('models');
+ const prompts = { select, cancel: vi.fn() };
+
+ await expect(runKtxSetupReadyMenu(readyStatus, { prompts })).resolves.toEqual({ action: 'models' });
+
+ expect(select).toHaveBeenCalledTimes(2);
+ expect(select).toHaveBeenLastCalledWith({
+ message: 'What would you like to change?',
options: [
{ value: 'models', label: 'Models' },
{ value: 'embeddings', label: 'Embeddings' },
@@ -51,3 +81,39 @@ describe('setup ready menu', () => {
});
});
});
+
+describe('runKtxSetupReadyChangeMenu', () => {
+ it('maps ready-project menu choices to setup sections', async () => {
+ const prompts = { select: vi.fn(async () => 'agents'), cancel: vi.fn() };
+
+ await expect(runKtxSetupReadyChangeMenu(readyStatus, { prompts })).resolves.toEqual({ action: 'agents' });
+
+ expect(prompts.select).toHaveBeenCalledWith({
+ message: 'What would you like to change?',
+ options: [
+ { value: 'models', label: 'Models' },
+ { value: 'embeddings', label: 'Embeddings' },
+ { value: 'databases', label: 'Databases' },
+ { value: 'sources', label: 'Context sources' },
+ { value: 'context', label: 'Rebuild KTX context' },
+ { value: 'agents', label: 'Agent integration' },
+ { value: 'exit', label: 'Exit' },
+ ],
+ });
+ });
+
+ it('includes the runtime option only when the runtime is required', async () => {
+ const prompts = { select: vi.fn(async () => 'runtime'), cancel: vi.fn() };
+
+ await runKtxSetupReadyChangeMenu(
+ { ...readyStatus, runtime: { required: true, ready: true, features: ['core'] } },
+ { prompts },
+ );
+
+ expect(prompts.select).toHaveBeenCalledWith(
+ expect.objectContaining({
+ options: expect.arrayContaining([{ value: 'runtime', label: 'Runtime' }]),
+ }),
+ );
+ });
+});
diff --git a/packages/cli/test/setup.test.ts b/packages/cli/test/setup.test.ts
index da51e9af..e4eca44d 100644
--- a/packages/cli/test/setup.test.ts
+++ b/packages/cli/test/setup.test.ts
@@ -2205,8 +2205,11 @@ describe('setup status', () => {
join(tempDir, 'ktx.yaml'),
[
'setup:',
- ' database_connection_ids: []',
- 'connections: {}',
+ ' database_connection_ids: [warehouse]',
+ 'connections:',
+ ' warehouse:',
+ ' driver: postgres',
+ ' url: env:DATABASE_URL',
'llm:',
' provider:',
' backend: anthropic',
@@ -2222,7 +2225,7 @@ describe('setup status', () => {
'utf-8',
);
await writeKtxSetupState(tempDir, {
- completed_steps: ['project', 'llm', 'embeddings', 'sources', 'runtime', 'context', 'agents'],
+ completed_steps: ['project', 'llm', 'embeddings', 'databases', 'sources', 'runtime', 'context', 'agents'],
});
await writeFile(
join(tempDir, '.ktx/agents/install-manifest.json'),
@@ -2275,7 +2278,12 @@ describe('setup status', () => {
},
io.io,
{
- readyMenuDeps: { prompts: { select: vi.fn(async () => 'agents'), cancel: vi.fn() } },
+ readyMenuDeps: {
+ prompts: {
+ select: vi.fn().mockResolvedValueOnce('change').mockResolvedValueOnce('agents'),
+ cancel: vi.fn(),
+ },
+ },
model: async (args) => {
expect(args.skipLlm).toBe(true);
return { status: 'skipped', projectDir: tempDir };
@@ -2325,8 +2333,11 @@ describe('setup status', () => {
join(tempDir, 'ktx.yaml'),
[
'setup:',
- ' database_connection_ids: []',
- 'connections: {}',
+ ' database_connection_ids: [warehouse]',
+ 'connections:',
+ ' warehouse:',
+ ' driver: postgres',
+ ' url: env:DATABASE_URL',
'llm:',
' provider:',
' backend: anthropic',
@@ -2342,7 +2353,7 @@ describe('setup status', () => {
'utf-8',
);
await writeKtxSetupState(tempDir, {
- completed_steps: ['project', 'llm', 'embeddings', 'sources', 'context'],
+ completed_steps: ['project', 'llm', 'embeddings', 'databases', 'sources', 'context'],
});
await writeKtxSetupContextState(tempDir, {
runId: 'setup-context-local-ready',
@@ -2415,6 +2426,171 @@ describe('setup status', () => {
expect(calls).toEqual(['agents']);
});
+ it('routes a returning user to the context build when config is ready but context is not built', async () => {
+ const calls: string[] = [];
+ const io = makeIo();
+ await writeFile(
+ join(tempDir, 'ktx.yaml'),
+ [
+ 'setup:',
+ ' database_connection_ids: [warehouse]',
+ 'connections:',
+ ' warehouse:',
+ ' driver: postgres',
+ ' url: env:DATABASE_URL',
+ 'llm:',
+ ' provider:',
+ ' backend: anthropic',
+ ' models:',
+ ' default: claude-sonnet-4-6',
+ 'ingest:',
+ ' embeddings:',
+ ' backend: openai',
+ ' model: text-embedding-3-small',
+ ' dimensions: 1536',
+ '',
+ ].join('\n'),
+ 'utf-8',
+ );
+ await writeKtxSetupState(tempDir, {
+ completed_steps: ['project', 'llm', 'embeddings', 'databases', 'sources', 'runtime'],
+ });
+
+ const readyMenuSelect = vi.fn();
+ await expect(
+ runKtxSetup(
+ {
+ command: 'run',
+ projectDir: tempDir,
+ mode: 'auto',
+ agents: false,
+ inputMode: 'auto',
+ yes: false,
+ cliVersion: '0.2.0',
+ skipLlm: false,
+ skipEmbeddings: false,
+ skipDatabases: false,
+ skipSources: false,
+ skipAgents: false,
+ databaseSchemas: [],
+ },
+ io.io,
+ {
+ readyMenuDeps: { prompts: { select: readyMenuSelect, cancel: vi.fn() } },
+ model: async (args) => {
+ expect(args.skipLlm).toBe(true);
+ return { status: 'skipped', projectDir: tempDir };
+ },
+ embeddings: async (args) => {
+ expect(args.skipEmbeddings).toBe(true);
+ return { status: 'skipped', projectDir: tempDir };
+ },
+ databases: async (args) => {
+ expect(args.skipDatabases).toBe(true);
+ return { status: 'skipped', projectDir: tempDir };
+ },
+ sources: async (args) => {
+ expect(args.skipSources).toBe(true);
+ return { status: 'skipped', projectDir: tempDir };
+ },
+ runtime: async () => {
+ calls.push('runtime');
+ return runtimeReady(tempDir);
+ },
+ context: async (args) => {
+ calls.push('context');
+ expect(args.forcePrompt).toBe(true);
+ return { status: 'skipped', projectDir: tempDir };
+ },
+ agents: async () => {
+ calls.push('agents');
+ return { status: 'ready', projectDir: tempDir, installs: [] };
+ },
+ },
+ ),
+ ).resolves.toBe(0);
+
+ // Config is done, so the change-everything menu is not shown; setup routes straight
+ // to the build prompt and never re-walks config or installs agents.
+ expect(readyMenuSelect).not.toHaveBeenCalled();
+ expect(calls).toContain('context');
+ expect(calls).not.toContain('agents');
+ const output = io.stdout();
+ expect(output).toContain('Setup is complete. The only step left is to build context');
+ expect(output).toContain('ktx ingest');
+ });
+
+ it('reaches the completion screen instead of a bare shell when the context build is skipped', async () => {
+ const calls: string[] = [];
+ const io = makeIo();
+ await writeFile(
+ join(tempDir, 'ktx.yaml'),
+ [
+ 'setup:',
+ ' database_connection_ids: [warehouse]',
+ 'connections:',
+ ' warehouse:',
+ ' driver: postgres',
+ ' url: env:DATABASE_URL',
+ 'llm:',
+ ' provider:',
+ ' backend: anthropic',
+ ' models:',
+ ' default: claude-sonnet-4-6',
+ 'ingest:',
+ ' embeddings:',
+ ' backend: openai',
+ ' model: text-embedding-3-small',
+ ' dimensions: 1536',
+ '',
+ ].join('\n'),
+ 'utf-8',
+ );
+ await writeKtxSetupState(tempDir, {
+ completed_steps: ['project', 'llm', 'embeddings', 'databases', 'sources', 'runtime'],
+ });
+
+ await expect(
+ runKtxSetup(
+ {
+ command: 'run',
+ projectDir: tempDir,
+ mode: 'auto',
+ agents: false,
+ inputMode: 'disabled',
+ yes: true,
+ cliVersion: '0.2.0',
+ skipLlm: true,
+ skipEmbeddings: true,
+ skipDatabases: true,
+ skipSources: true,
+ skipAgents: false,
+ databaseSchemas: [],
+ },
+ io.io,
+ {
+ model: async () => ({ status: 'skipped', projectDir: tempDir }),
+ embeddings: async () => ({ status: 'skipped', projectDir: tempDir }),
+ databases: async () => ({ status: 'skipped', projectDir: tempDir }),
+ sources: async () => ({ status: 'skipped', projectDir: tempDir }),
+ runtime: async () => runtimeReady(tempDir),
+ context: async () => ({ status: 'skipped', projectDir: tempDir }),
+ agents: async () => {
+ calls.push('agents');
+ return { status: 'ready', projectDir: tempDir, installs: [] };
+ },
+ },
+ ),
+ ).resolves.toBe(0);
+
+ // A skipped build must not install agents nor drop to a bare shell; the end screen
+ // states readiness and points at `ktx ingest`.
+ expect(calls).not.toContain('agents');
+ const output = io.stdout();
+ expect(output).toContain('Setup is complete. The only step left is to build context');
+ expect(output).toContain('ktx ingest');
+ });
+
it('runs only project resolution and agent setup in --agents mode', async () => {
const io = makeIo();
const runtime = vi.fn(async () => runtimeReady(tempDir));
From 9d3a0b751df68c19df8007c4dec4c891f73246b0 Mon Sep 17 00:00:00 2001
From: "github-actions[bot]"
<41898282+github-actions[bot]@users.noreply.github.com>
Date: Wed, 3 Jun 2026 07:50:39 +0000
Subject: [PATCH 10/49] chore: refresh star history chart [skip ci]
---
assets/star-history.svg | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/assets/star-history.svg b/assets/star-history.svg
index d6d9859e..23016f3e 100644
--- a/assets/star-history.svg
+++ b/assets/star-history.svg
@@ -1 +1 @@
-star-history.com May 17 May 24 May 31 200 400 600 kaelio/ktx Star History Date GitHub Stars
+star-history.com May 17 May 24 May 31 200 400 600 800 kaelio/ktx Star History Date GitHub Stars
From f5dea9a0891305e7c4d90b0156638681fe75c1dc Mon Sep 17 00:00:00 2001
From: Andrey Avtomonov
Date: Wed, 3 Jun 2026 13:05:59 +0200
Subject: [PATCH 11/49] fix(ingest): recover textual-conflict gate failures;
fix query-history adapter (#255)
* fix(ingest): recover textual-conflict gate failures; fix query-history adapter
Two latent gaps in the isolated-diff local-ingest pipeline that can abort an
otherwise-successful ingest:
- Metabase: when a work-unit patch hit both a textual conflict and a post-merge
dangling sl_ref, the after-textual-resolution branch returned a hard
semantic_conflict and rolled back the whole job. It now runs the same
repairGateFailure recovery the clean-apply branch already uses (re-validate,
then commit the union of resolved + repaired paths), reaching parity.
- Query history: the historic-sql adapter was registered only when ktx.yaml had
context.queryHistory.enabled=true, so `--query-history` threw "Adapter not
available for local ingest". Registration now resolves the dialect from driver
capability, since the explicit --query-history request is itself the opt-in;
the config-gated helper is unchanged for status/setup/probes.
Adds the previously-missing tests for both paths.
* chore: sync uv.lock to 0.8.0 (regenerated with pinned uv 0.11.11)
* fix(ingest): drop ktx's own scan probes and dedup tables in query history
Query history (historic-sql) mined two kinds of noise back into context:
- ktx's own warehouse scan emits relationship- and column-profiling probes
(the relationship_profile_values aggregation and the child_values/parent_values
FK-overlap CTEs) into pg_stat_statements. shouldDropBySql now filters these
ktx-owned, dialect-stable signatures so ktx introspection is not ingested as
usage history.
- The same physical table appears both bare (accounts, via search_path) and
schema-qualified (orbit_raw.accounts), producing duplicate per-table work
units. canonicalizeTableIdentifiers collapses a bare name into its unique
qualified form before work-unit keying; ambiguous names are left untouched.
On the orbit demo this removes ~35% of sampled query templates (ktx self-probes)
and ~45 duplicate per-table work units.
* docs(agents): add Design Reasoning Defaults section
---
AGENTS.md | 59 ++++++++++++
.../historic-sql/connection-dialect.ts | 20 +++-
.../adapters/historic-sql/stage-unified.ts | 62 ++++++++++++
.../ingest/isolated-diff/patch-integrator.ts | 95 ++++++++++++++++++-
packages/cli/src/local-adapters.ts | 9 +-
.../historic-sql/connection-dialect.test.ts | 21 +++-
.../historic-sql/stage-unified.test.ts | 84 ++++++++++++++++
.../isolated-diff/patch-integrator.test.ts | 68 +++++++++++++
packages/cli/test/local-adapters.test.ts | 31 ++++++
9 files changed, 437 insertions(+), 12 deletions(-)
diff --git a/AGENTS.md b/AGENTS.md
index 0cd9da93..20f9bcdf 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -159,6 +159,65 @@ and naming asymmetries are bugs in waiting — see
[`docs/code-design.md`](docs/code-design.md). Treat the `MUST` / `MUST NOT`
rules there with the same weight as the ones in this file.
+## Design Reasoning Defaults
+
+When proposing a design, an approach, or any non-trivial change, apply these
+defaults and run the self-check before presenting it. They encode the
+corrections users most often have to make; reaching these conclusions
+autonomously — without being asked the leading question — is the bar.
+
+- **MUST**: Optimize for the best outcome, not for an unstated constraint. Do not
+ silently adopt "smallest change", "least effort", "cheapest", or "least user
+ intervention" as the goal unless the user said so. Default to the most correct,
+ durable solution, and present cost / effort / scope as information for the user
+ to weigh — not as a ceiling you impose on their behalf.
+- **MUST**: Separate one-time cost from recurring cost before discarding an
+ option. A fixed cost paid once (a setup-time computation, an extra LLM call
+ during setup, a contract change) to make every later run cheaper or more
+ correct is usually worth it. Do not reject it with recurring-cost reasoning;
+ quantify both sides. (Example smell: "don't add an LLM call to a cost-cutting
+ feature" — wrong when the call is one-time and the savings recur.)
+- **MUST**: Treat a user's example as a representative of a class, not as the
+ spec. Design for the general population the example stands for, then stress-test
+ against deliberately different instances — another warehouse, dialect, stack
+ layout, or input shape — before committing. If a design only works because of an
+ incidental property of the example (e.g. "the noise happened to be in a separate
+ schema *on this demo*"), it is curve-fitting; generalize it or state the
+ assumption explicitly.
+- **MUST**: Prefer deriving from the system's own state over enumerating cases.
+ Favor an allowlist computed from declared/observed state (config, scanned
+ catalog, query log, the user's own inputs) over a denylist of known-bad
+ specifics (particular tables, schemas, tools, or vendors). A hardcoded or
+ hand-maintained list of external specifics is a smell: it rots and fails on the
+ next stack. The only acceptable static patterns are genuinely universal
+ invariants (e.g. DB-engine system catalogs) and ktx's own self-emitted
+ signatures.
+- **SHOULD**: Before inventing an abstraction or hand-rolling structural logic,
+ search for what already exists and reuse it — the codebase's canonical
+ representation (a structured ref/key type) instead of a parallel string scheme,
+ and a mandated/available tool (e.g. `sqlglot` for SQL structure; see
+ [SQL and Structured Parsing](#sql-and-structured-parsing)) instead of
+ hand-parsing. Normalize ambiguous input to the canonical form at the boundary;
+ do not carry the ambiguity downstream. This is the single-source-of-truth / DRY
+ item from the Priority Hierarchy applied at design time.
+
+Before presenting a design, answer these explicitly:
+
+1. Am I optimizing for a goal the user actually stated, or one I assumed?
+2. Does this generalize beyond the example in front of me? Name a real case where
+ it would break.
+3. Am I enumerating known-bad cases when I could derive scope from the system's
+ own declared/observed state?
+4. Is there an existing canonical representation or mandated tool I should reuse
+ instead of building or parsing my own?
+5. Am I discarding the better option on a weak or misapplied constraint
+ (one-time vs recurring cost, "more surface area", "more work now")?
+
+A user question that nudges toward an alternative ("would X help?", "should I
+always do Y?", "will you hardcode Z?") is a signal that a better option exists.
+Investigate the implied direction and reason it through *before* defending the
+original proposal — and prefer to have asked yourself the question first.
+
## TypeScript Standards
- Use Node 22+ and pnpm workspace commands.
diff --git a/packages/cli/src/context/ingest/adapters/historic-sql/connection-dialect.ts b/packages/cli/src/context/ingest/adapters/historic-sql/connection-dialect.ts
index dd95f87a..7845cbbc 100644
--- a/packages/cli/src/context/ingest/adapters/historic-sql/connection-dialect.ts
+++ b/packages/cli/src/context/ingest/adapters/historic-sql/connection-dialect.ts
@@ -26,6 +26,21 @@ export function isQueryHistoryEnabled(connection: unknown): boolean {
return queryHistoryRecord(connection)?.enabled === true;
}
+/**
+ * Resolves the query-history dialect from the connection's driver capability
+ * alone, ignoring whether query history is enabled in ktx.yaml. Use this on the
+ * adapter-registration path when query history has been explicitly requested
+ * for the run (e.g. via `--query-history`, which is itself the opt-in): the
+ * persisted `context.queryHistory.enabled` flag must not gate registration.
+ * Returns null when the connection's driver has no query-history reader.
+ */
+export function historicSqlDialectForConnectionDriver(connection: unknown): HistoricSqlDialect | null {
+ const conn = recordOrNull(connection);
+ const driver = String(conn?.driver ?? '').toLowerCase();
+ const registration = getDriverRegistration(driver);
+ return registration?.hasHistoricSqlReader ? historicSqlDialectForDriver(registration.driver) : null;
+}
+
/**
* Resolves the query-history dialect for a connection. Returns null when
* query history is disabled, or when the connection's driver has no
@@ -35,8 +50,5 @@ export function queryHistoryDialectForConnection(connection: unknown): HistoricS
if (!isQueryHistoryEnabled(connection)) {
return null;
}
- const conn = recordOrNull(connection);
- const driver = String(conn?.driver ?? '').toLowerCase();
- const registration = getDriverRegistration(driver);
- return registration?.hasHistoricSqlReader ? historicSqlDialectForDriver(registration.driver) : null;
+ return historicSqlDialectForConnectionDriver(connection);
}
diff --git a/packages/cli/src/context/ingest/adapters/historic-sql/stage-unified.ts b/packages/cli/src/context/ingest/adapters/historic-sql/stage-unified.ts
index 70997648..853a3e68 100644
--- a/packages/cli/src/context/ingest/adapters/historic-sql/stage-unified.ts
+++ b/packages/cli/src/context/ingest/adapters/historic-sql/stage-unified.ts
@@ -79,8 +79,21 @@ function matchesAny(value: string | null, patterns: RegExp[]): boolean {
return !!value && patterns.some((pattern) => pattern.test(value));
}
+// ktx's own warehouse scan emits relationship- and column-profiling probes that land in
+// pg_stat_statements (relationship-validation, relationship-composite-candidates, and each
+// dialect's relationship value aggregation). They are ktx introspection, not genuine query
+// usage, so they must not be mined back as query history. The markers are ktx-owned
+// identifiers, stable across dialects.
+function isKtxScanProbe(sql: string): boolean {
+ if (/\brelationship_profile_values\b/i.test(sql)) {
+ return true;
+ }
+ return /\bchild_values\b/i.test(sql) && /\bparent_values\b/i.test(sql);
+}
+
function shouldDropBySql(sql: string, config: HistoricSqlUnifiedPullConfig): boolean {
if (NOISE_PREFIX_RE.test(sql) || SYSTEM_TABLE_RE.test(sql)) return true;
+ if (isKtxScanProbe(sql)) return true;
if (config.filters.dropTrivialProbes !== false && TRIVIAL_SQL_RE.test(sql)) return true;
return false;
}
@@ -148,6 +161,53 @@ function isEnabledTable(table: string, filter: EnabledTableFilter | null): boole
return filter.exact.has(normalized) || filter.uniqueUnqualified.has(unqualifiedTableIdentifier(normalized));
}
+/**
+ * pg_stat_statements records queries as written, so the same physical table can appear
+ * both bare (`accounts`, resolved via search_path) and schema-qualified
+ * (`orbit_raw.accounts`). Collapse a bare identifier into its schema-qualified form when
+ * exactly one qualified form shares its unqualified name, so the two never become separate
+ * work units. Ambiguous bare names (two qualified forms) are left untouched.
+ */
+function canonicalizeTableIdentifiers(parsedTemplates: ParsedTemplate[]): void {
+ const all = new Set();
+ for (const parsed of parsedTemplates) {
+ for (const table of parsed.includedTables) {
+ all.add(table);
+ }
+ }
+ const qualifiedByUnqualified = new Map>();
+ for (const table of all) {
+ if (!table.includes('.')) {
+ continue;
+ }
+ const unqualified = unqualifiedTableIdentifier(table);
+ if (unqualified.length === 0) {
+ continue;
+ }
+ const forms = qualifiedByUnqualified.get(unqualified) ?? new Set();
+ forms.add(table);
+ qualifiedByUnqualified.set(unqualified, forms);
+ }
+ const canonical = new Map();
+ for (const table of all) {
+ if (table.includes('.')) {
+ continue;
+ }
+ const forms = qualifiedByUnqualified.get(unqualifiedTableIdentifier(table));
+ if (forms && forms.size === 1) {
+ canonical.set(table, [...forms][0]);
+ }
+ }
+ if (canonical.size === 0) {
+ return;
+ }
+ const remap = (table: string): string => canonical.get(table) ?? table;
+ for (const parsed of parsedTemplates) {
+ parsed.includedTables = [...new Set(parsed.includedTables.map(remap))].sort();
+ parsed.tablesTouched = [...new Set(parsed.tablesTouched.map(remap))].sort();
+ }
+}
+
function historicSqlWindowDays(config: HistoricSqlUnifiedPullConfig): number {
return 'windowDays' in config ? config.windowDays : 90;
}
@@ -323,6 +383,8 @@ export async function stageHistoricSqlAggregatedSnapshot(input: StageHistoricSql
});
}
+ canonicalizeTableIdentifiers(parsedTemplates);
+
const byTable = new Map();
for (const parsed of parsedTemplates) {
for (const table of parsed.includedTables) {
diff --git a/packages/cli/src/context/ingest/isolated-diff/patch-integrator.ts b/packages/cli/src/context/ingest/isolated-diff/patch-integrator.ts
index 869c019e..1e2f0cee 100644
--- a/packages/cli/src/context/ingest/isolated-diff/patch-integrator.ts
+++ b/packages/cli/src/context/ingest/isolated-diff/patch-integrator.ts
@@ -155,18 +155,103 @@ export async function integrateWorkUnitPatch(input: IntegrateWorkUnitPatchInput)
},
);
} catch (semanticError) {
- if (preApplyHead) {
- await input.integrationGit.resetHardTo(preApplyHead);
- }
+ const reason = errorMessage(semanticError);
await input.trace.event('error', 'integration', 'patch_semantic_conflict_after_textual_resolution', {
unitKey: input.unitKey,
patchPath: input.patchPath,
touchedPaths: textualResolution.changedPaths,
- reason: errorMessage(semanticError),
+ reason,
});
+
+ // A textual conflict and a semantic-gate failure can co-occur: the resolver
+ // reconciles the text but can leave wiki sl_refs pointing at measures the
+ // merged source no longer defines. Recover via the same gate repair the
+ // clean-apply branch uses, instead of hard-failing the whole job.
+ if (input.repairGateFailure) {
+ const gateRepair = await input.repairGateFailure({
+ unitKey: input.unitKey,
+ patchPath: input.patchPath,
+ touchedPaths: textualResolution.changedPaths,
+ reason,
+ });
+
+ if (gateRepair.status !== 'failed') {
+ // The resolver wrote its merge to the worktree (unstaged); the repair
+ // edited a subset on top. Commit the union so neither is dropped.
+ const resolvedAndRepairedPaths = [
+ ...new Set([...textualResolution.changedPaths, ...gateRepair.changedPaths]),
+ ].sort();
+ try {
+ await traceTimed(
+ input.trace,
+ 'integration',
+ 'semantic_gate_after_gate_repair',
+ { unitKey: input.unitKey, touchedPaths: gateRepair.changedPaths },
+ async () => {
+ await input.validateAppliedTree(gateRepair.changedPaths);
+ },
+ );
+
+ const commit = await input.integrationGit.commitFiles(
+ resolvedAndRepairedPaths,
+ `ingest: resolve WorkUnit ${input.unitKey} conflict`,
+ input.author.name,
+ input.author.email,
+ );
+ if (commit.created) {
+ await input.trace.event('debug', 'integration', 'patch_accepted_after_textual_resolution', {
+ unitKey: input.unitKey,
+ commitSha: commit.commitHash,
+ touchedPaths: resolvedAndRepairedPaths,
+ attempts: textualResolution.attempts,
+ gateRepairAttempts: gateRepair.attempts,
+ });
+ return {
+ status: 'accepted',
+ commitSha: commit.commitHash,
+ touchedPaths: resolvedAndRepairedPaths,
+ textualResolution,
+ gateRepair,
+ };
+ }
+ } catch (repairValidationError) {
+ if (preApplyHead) {
+ await input.integrationGit.resetHardTo(preApplyHead);
+ }
+ await input.trace.event('error', 'integration', 'patch_semantic_conflict_after_textual_resolution', {
+ unitKey: input.unitKey,
+ patchPath: input.patchPath,
+ touchedPaths: gateRepair.changedPaths,
+ reason: errorMessage(repairValidationError),
+ });
+ return {
+ status: 'semantic_conflict',
+ reason: errorMessage(repairValidationError),
+ touchedPaths: gateRepair.changedPaths,
+ textualResolution,
+ gateRepair,
+ };
+ }
+ }
+
+ if (preApplyHead) {
+ await input.integrationGit.resetHardTo(preApplyHead);
+ }
+ return {
+ status: 'semantic_conflict',
+ reason: gateRepair.status === 'failed' ? gateRepair.reason : reason,
+ touchedPaths: textualResolution.changedPaths,
+ textualResolution,
+ gateRepair,
+ };
+ }
+
+ if (preApplyHead) {
+ await input.integrationGit.resetHardTo(preApplyHead);
+ }
return {
status: 'semantic_conflict',
- reason: errorMessage(semanticError),
+ reason,
touchedPaths: textualResolution.changedPaths,
textualResolution,
};
diff --git a/packages/cli/src/local-adapters.ts b/packages/cli/src/local-adapters.ts
index cfc57adc..0cd2d940 100644
--- a/packages/cli/src/local-adapters.ts
+++ b/packages/cli/src/local-adapters.ts
@@ -12,7 +12,7 @@ import { isKtxSqliteConnectionConfig } from './connectors/sqlite/connector.js';
import { createSqlServerLiveDatabaseIntrospection } from './connectors/sqlserver/live-database-introspection.js';
import { isKtxSqlServerConnectionConfig } from './connectors/sqlserver/connector.js';
import { BigQueryHistoricSqlQueryHistoryReader } from './context/ingest/adapters/historic-sql/bigquery-query-history-reader.js';
-import { queryHistoryDialectForConnection } from './context/ingest/adapters/historic-sql/connection-dialect.js';
+import { historicSqlDialectForConnectionDriver } from './context/ingest/adapters/historic-sql/connection-dialect.js';
import { createDaemonLiveDatabaseIntrospection } from './context/ingest/adapters/live-database/daemon-introspection.js';
import { createDefaultLocalIngestAdapters, type DefaultLocalIngestAdaptersOptions } from './context/ingest/local-adapters.js';
import type { HistoricSqlReader } from './context/ingest/adapters/historic-sql/types.js';
@@ -268,7 +268,12 @@ function historicSqlOptionsForLocalRun(project: KtxLocalProject, options: KtxCli
return undefined;
}
const connection = project.config.connections[connectionId];
- const dialect = queryHistoryDialectForConnection(connection);
+ // historicSqlConnectionId is only set when query history was explicitly
+ // requested for this run (e.g. `--query-history`), so resolve the dialect from
+ // driver capability rather than the persisted context.queryHistory.enabled
+ // flag — otherwise the adapter is missing and findAdapter('historic-sql')
+ // throws even though the run asked for it.
+ const dialect = historicSqlDialectForConnectionDriver(connection);
if (!dialect) {
return undefined;
}
diff --git a/packages/cli/test/context/ingest/adapters/historic-sql/connection-dialect.test.ts b/packages/cli/test/context/ingest/adapters/historic-sql/connection-dialect.test.ts
index 8dc2ec88..935bab8e 100644
--- a/packages/cli/test/context/ingest/adapters/historic-sql/connection-dialect.test.ts
+++ b/packages/cli/test/context/ingest/adapters/historic-sql/connection-dialect.test.ts
@@ -1,5 +1,8 @@
import { describe, expect, it } from 'vitest';
-import { queryHistoryDialectForConnection } from '../../../../../src/context/ingest/adapters/historic-sql/connection-dialect.js';
+import {
+ historicSqlDialectForConnectionDriver,
+ queryHistoryDialectForConnection,
+} from '../../../../../src/context/ingest/adapters/historic-sql/connection-dialect.js';
describe('queryHistoryDialectForConnection', () => {
it.each([
@@ -21,3 +24,19 @@ describe('queryHistoryDialectForConnection', () => {
expect(queryHistoryDialectForConnection({ driver: 'postgres', context: { queryHistory: { enabled: false } } })).toBeNull();
});
});
+
+describe('historicSqlDialectForConnectionDriver', () => {
+ it('resolves the dialect from driver capability even when query history is disabled', () => {
+ expect(
+ historicSqlDialectForConnectionDriver({ driver: 'postgres', context: { queryHistory: { enabled: false } } }),
+ ).toBe('postgres');
+ });
+
+ it('resolves the dialect when no query-history context is present', () => {
+ expect(historicSqlDialectForConnectionDriver({ driver: 'bigquery' })).toBe('bigquery');
+ });
+
+ it('returns null for drivers without a historic-SQL reader', () => {
+ expect(historicSqlDialectForConnectionDriver({ driver: 'mysql', context: { queryHistory: { enabled: true } } })).toBeNull();
+ });
+});
diff --git a/packages/cli/test/context/ingest/adapters/historic-sql/stage-unified.test.ts b/packages/cli/test/context/ingest/adapters/historic-sql/stage-unified.test.ts
index b930d695..630a3939 100644
--- a/packages/cli/test/context/ingest/adapters/historic-sql/stage-unified.test.ts
+++ b/packages/cli/test/context/ingest/adapters/historic-sql/stage-unified.test.ts
@@ -433,4 +433,88 @@ describe('stageHistoricSqlAggregatedSnapshot', () => {
const manifest = await readJson>(stagedDir, 'manifest.json');
expect(manifest.warnings).toEqual([]);
});
+
+ it("drops ktx's own scan/relationship probes from query history", async () => {
+ const stagedDir = await tempDir();
+ const fkOverlapProbe =
+ 'select * from (WITH child_values AS ( SELECT DISTINCT "account_id" AS value FROM "account_owners" WHERE "account_id" IS NOT NULL LIMIT $1 ), parent_values AS ( SELECT DISTINCT "account_id" AS value FROM "accounts" WHERE "account_id" IS NOT NULL ) SELECT (SELECT COUNT(*) FROM child_values) AS child_distinct, (SELECT COUNT(*) FROM parent_values) AS parent_distinct) probe';
+ const profileProbe =
+ 'select * from (SELECT $1 AS column_name, (SELECT COUNT(*) FROM "orbit_raw"."accounts") AS total, (SELECT STRING_AGG(CAST(value AS TEXT), CHR(31)) FROM (SELECT DISTINCT "id" AS value FROM "orbit_raw"."accounts" LIMIT $2) AS relationship_profile_values) AS samples) profile';
+ const reader: HistoricSqlReader = {
+ async probe() {
+ return { warnings: [], info: [] };
+ },
+ async *fetchAggregated() {
+ yield aggregate({
+ templateId: 'analytic',
+ canonicalSql: 'select status, count(*) from public.orders group by status',
+ });
+ yield aggregate({ templateId: 'ktx-fk-overlap', canonicalSql: fkOverlapProbe });
+ yield aggregate({ templateId: 'ktx-profile', canonicalSql: profileProbe });
+ },
+ };
+ const sqlAnalysis: SqlAnalysisPort = {
+ analyzeForFingerprint: vi.fn(),
+ analyzeBatch: vi.fn(async () => new Map([
+ ['analytic', { tablesTouched: ['public.orders'], columnsByClause: { select: ['status'], where: [], join: [], groupBy: ['status'] } }],
+ ])),
+ validateReadOnly: vi.fn(async () => ({ ok: true })),
+ };
+
+ await stageHistoricSqlAggregatedSnapshot({
+ stagedDir,
+ connectionId: 'warehouse',
+ queryClient: {},
+ reader,
+ sqlAnalysis,
+ pullConfig: { dialect: 'postgres' },
+ now: new Date('2026-05-11T12:00:00.000Z'),
+ });
+
+ // ktx scan probes are filtered before SQL analysis, so only the analytic query is parsed.
+ expect(sqlAnalysis.analyzeBatch).toHaveBeenCalledWith(
+ [{ id: 'analytic', sql: 'select status, count(*) from public.orders group by status' }],
+ 'postgres',
+ );
+ expect(await readdir(join(stagedDir, 'tables'))).toEqual(['public.orders.json']);
+ });
+
+ it('merges bare and schema-qualified references to the same table into one work unit', async () => {
+ const stagedDir = await tempDir();
+ const reader: HistoricSqlReader = {
+ async probe() {
+ return { warnings: [], info: [] };
+ },
+ async *fetchAggregated() {
+ yield aggregate({ templateId: 'qualified', canonicalSql: 'select count(*) from orbit_raw.accounts' });
+ yield aggregate({ templateId: 'bare', canonicalSql: 'select id from accounts where active' });
+ },
+ };
+ const sqlAnalysis: SqlAnalysisPort = {
+ analyzeForFingerprint: vi.fn(),
+ analyzeBatch: vi.fn(async () => new Map([
+ ['qualified', { tablesTouched: ['orbit_raw.accounts'], columnsByClause: { select: [], where: [], join: [], groupBy: [] } }],
+ ['bare', { tablesTouched: ['accounts'], columnsByClause: { select: ['id'], where: ['active'], join: [], groupBy: [] } }],
+ ])),
+ validateReadOnly: vi.fn(async () => ({ ok: true })),
+ };
+
+ await stageHistoricSqlAggregatedSnapshot({
+ stagedDir,
+ connectionId: 'warehouse',
+ queryClient: {},
+ reader,
+ sqlAnalysis,
+ pullConfig: { dialect: 'postgres' },
+ now: new Date('2026-05-11T12:00:00.000Z'),
+ });
+
+ // The bare `accounts` reference resolves to the unique qualified `orbit_raw.accounts`,
+ // so the two templates collapse into a single work unit instead of two.
+ expect(await readdir(join(stagedDir, 'tables'))).toEqual(['orbit_raw.accounts.json']);
+ const merged = await readJson>(stagedDir, 'tables/orbit_raw.accounts.json');
+ expect(merged.topTemplates.map((t: any) => t.id).sort()).toEqual(['bare', 'qualified']);
+ const manifest = await readJson>(stagedDir, 'manifest.json');
+ expect(manifest.touchedTableCount).toBe(1);
+ });
});
diff --git a/packages/cli/test/context/ingest/isolated-diff/patch-integrator.test.ts b/packages/cli/test/context/ingest/isolated-diff/patch-integrator.test.ts
index 1deabfe8..e822472d 100644
--- a/packages/cli/test/context/ingest/isolated-diff/patch-integrator.test.ts
+++ b/packages/cli/test/context/ingest/isolated-diff/patch-integrator.test.ts
@@ -401,4 +401,72 @@ describe('integrateWorkUnitPatch', () => {
});
await expect(readFile(join(configDir, 'wiki/global/a.md'), 'utf-8')).resolves.toBe('old\n');
});
+
+ it('repairs a semantic gate failure after a textual conflict is resolved', async () => {
+ const { homeDir, configDir, git } = await makeRepo();
+ await mkdir(join(configDir, 'wiki/global'), { recursive: true });
+ await writeFile(join(configDir, 'wiki/global/a.md'), 'base\n', 'utf-8');
+ await git.commitFiles(['wiki/global/a.md'], 'base page', 'System User', 'system@example.com');
+ const conflictBase = await git.revParseHead();
+
+ await writeFile(join(configDir, 'wiki/global/a.md'), 'accepted\n', 'utf-8');
+ await git.commitFiles(['wiki/global/a.md'], 'accepted edit', 'System User', 'system@example.com');
+
+ const childDir = join(homeDir, 'child-conflict-repair');
+ await git.addWorktree(childDir, 'child-conflict-repair', conflictBase);
+ const childGit = git.forWorktree(childDir);
+ await writeFile(join(childDir, 'wiki/global/a.md'), 'proposal\n', 'utf-8');
+ await childGit.commitFiles(['wiki/global/a.md'], 'proposal edit', 'System User', 'system@example.com');
+ const patchPath = join(homeDir, 'proposal-repair.patch');
+ await childGit.writeBinaryNoRenamePatch(conflictBase, 'HEAD', patchPath);
+
+ const trace = new FileIngestTraceWriter({
+ tracePath: join(homeDir, '.ktx/ingest-traces/job-resolver-repair/trace.jsonl'),
+ jobId: 'job-resolver-repair',
+ connectionId: 'warehouse',
+ sourceKey: 'metabase',
+ level: 'trace',
+ });
+
+ // Gate fails on the resolver's merged tree, then passes after the repair edit.
+ const validateAppliedTree = vi
+ .fn()
+ .mockRejectedValueOnce(
+ new Error('final artifact gates failed:\narr-definition: unknown sl_refs entity mart_arr_daily.arr_dollars'),
+ )
+ .mockResolvedValueOnce(undefined);
+
+ const repairGateFailure = vi.fn(async (context: { unitKey: string; touchedPaths: string[] }) => {
+ expect(context).toMatchObject({ unitKey: 'wu-conflict-repair', touchedPaths: ['wiki/global/a.md'] });
+ await writeFile(join(configDir, 'wiki/global/a.md'), 'accepted\nproposal repaired\n', 'utf-8');
+ return { status: 'repaired' as const, attempts: 1, changedPaths: ['wiki/global/a.md'] };
+ });
+
+ const result = await integrateWorkUnitPatch({
+ unitKey: 'wu-conflict-repair',
+ patchPath,
+ integrationGit: git,
+ trace,
+ author: { name: 'System User', email: 'system@example.com' },
+ slDisallowed: false,
+ allowedTargetConnectionIds: new Set(['warehouse']),
+ validateAppliedTree,
+ resolveTextualConflict: vi.fn(async () => {
+ await writeFile(join(configDir, 'wiki/global/a.md'), 'accepted\nproposal\n', 'utf-8');
+ return { status: 'repaired' as const, attempts: 1, changedPaths: ['wiki/global/a.md'] };
+ }),
+ repairGateFailure,
+ });
+
+ expect(result).toMatchObject({
+ status: 'accepted',
+ touchedPaths: ['wiki/global/a.md'],
+ textualResolution: { status: 'repaired' },
+ gateRepair: { status: 'repaired', attempts: 1, changedPaths: ['wiki/global/a.md'] },
+ });
+ expect(validateAppliedTree).toHaveBeenCalledTimes(2);
+ expect(repairGateFailure).toHaveBeenCalledOnce();
+ await expect(readFile(join(configDir, 'wiki/global/a.md'), 'utf-8')).resolves.toBe('accepted\nproposal repaired\n');
+ await expect(readFile(trace.tracePath, 'utf-8')).resolves.toContain('patch_accepted_after_textual_resolution');
+ });
});
diff --git a/packages/cli/test/local-adapters.test.ts b/packages/cli/test/local-adapters.test.ts
index c7ae58cc..345f662a 100644
--- a/packages/cli/test/local-adapters.test.ts
+++ b/packages/cli/test/local-adapters.test.ts
@@ -70,6 +70,37 @@ describe('CLI local ingest adapters', () => {
]);
});
+ it('registers historic SQL when explicitly requested even if connection query history is disabled', async () => {
+ await writeProject(
+ tempDir,
+ [
+ 'connections:',
+ ' warehouse:',
+ ' driver: postgres',
+ ' url: env:WAREHOUSE_DATABASE_URL',
+ ' readonly: true',
+ ' context:',
+ ' queryHistory:',
+ ' enabled: false',
+ 'ingest:',
+ ' adapters:',
+ ' - historic-sql',
+ '',
+ ].join('\n'),
+ );
+ const project = await loadKtxProject({ projectDir: tempDir });
+
+ // `--query-history` sets historicSqlConnectionId for the run; that explicit
+ // request is the opt-in, so the persisted context.queryHistory.enabled flag
+ // must not gate adapter registration.
+ const adapters = createKtxCliLocalIngestAdapters(project, {
+ historicSqlConnectionId: 'warehouse',
+ sqlAnalysis: sqlAnalysisStub(),
+ });
+
+ expect(adapters.some((adapter) => adapter.source === 'historic-sql')).toBe(true);
+ });
+
it('registers BigQuery historic SQL from the requested connection', async () => {
await writeProject(
tempDir,
From ce1516b357807874902d189d1d163755634083e8 Mon Sep 17 00:00:00 2001
From: Andrey Avtomonov
Date: Wed, 3 Jun 2026 13:08:46 +0200
Subject: [PATCH 12/49] feat(cli): consistent connection setup recovery and
build-time gate (#257)
* feat(cli): block context build when a required connection fails its live test
A context build can take several minutes, so a connection that is
unreachable or misconfigured should stop the build up front instead of
failing partway through. Before the build starts, run a live connection
test for every primary- and context-source connection the build depends
on.
Each test's output is captured in a discarded buffer so raw error text
(and database paths) never reach the user; failures are surfaced only by
connection id and connector type, with a pointer to `ktx connection test
` for the underlying error.
- Interactive setup lets the user fix the connection and retry without
restarting, re-resolving targets so an added/removed/reconfigured
connection is honored.
- `--no-input` exits non-zero and writes a failed context state with a
failureReason, so scripts stop early and setup never reads as ready.
Extract the buffered command IO helper out of setup-databases into
src/io/buffered-command-io.ts so both call sites share one implementation.
* feat(cli): use recovery primitive for database setup
* feat(cli): use recovery primitive for source setup
* docs: document setup connection recovery
* fix(cli): close database recovery gaps
* fix(cli): target failing project in gate hint and preserve missing-input
Address two review findings on the connection-recovery work:
- The connection-gate failure hint emitted `ktx connection test ` with no
--project-dir, so a setup run started with `--project-dir ./analytics` pointed
users at cwd/KTX_PROJECT_DIR instead of the project that just failed. Emit the
resolved project dir, matching the contextBuildCommands convention.
- The non-interactive database configure path returned `cancelled`, which the
recovery primitive collapses to `failed`. Sibling paths still report
`missing-input` for absent flags, so incomplete-flag runs were
indistinguishable from real connection failures. The database wrapper now
tracks the configure missing-input signal and restores the `missing-input`
step status; the shared primitive keeps its four outcomes.
---
.../docs/cli-reference/ktx-connection.mdx | 4 +-
.../docs/getting-started/quickstart.mdx | 23 +-
packages/cli/src/connection-recovery.ts | 132 +++++
packages/cli/src/io/buffered-command-io.ts | 35 ++
packages/cli/src/setup-context.ts | 191 ++++++-
packages/cli/src/setup-databases.ts | 517 +++++++++---------
packages/cli/src/setup-sources.ts | 222 ++++++--
packages/cli/test/connection-recovery.test.ts | 171 ++++++
packages/cli/test/setup-context.test.ts | 117 +++-
packages/cli/test/setup-databases.test.ts | 300 +++++++++-
packages/cli/test/setup-sources.test.ts | 173 +++++-
11 files changed, 1531 insertions(+), 354 deletions(-)
create mode 100644 packages/cli/src/connection-recovery.ts
create mode 100644 packages/cli/src/io/buffered-command-io.ts
create mode 100644 packages/cli/test/connection-recovery.test.ts
diff --git a/docs-site/content/docs/cli-reference/ktx-connection.mdx b/docs-site/content/docs/cli-reference/ktx-connection.mdx
index 36185d68..9d78bdd8 100644
--- a/docs-site/content/docs/cli-reference/ktx-connection.mdx
+++ b/docs-site/content/docs/cli-reference/ktx-connection.mdx
@@ -104,6 +104,6 @@ configured connection and exit non-zero if any probe fails.
| Error | Cause | Recovery |
|-------|-------|----------|
| No connections configured | The project has no entries under `connections` | Run `ktx setup` and add a database or context-source connection |
-| Connection test fails | Credentials, network access, database, warehouse, or schema is invalid | Verify the same URL with the database's native client, then rerun `ktx setup` and reconfigure the connection |
-| Mapping validation fails during setup | BI database mappings do not point at valid warehouse connections | Rerun `ktx setup` and update the context-source mapping selections |
+| Connection test fails | Credentials, network access, database, warehouse, or schema is invalid | Use the setup recovery menu to retry or re-enter details; if it still fails, verify the same URL with the database's native client |
+| Mapping validation fails during setup | BI database mappings do not point at valid warehouse connections | Use the setup recovery menu to retry validation or re-enter mapping selections; rerun `ktx setup` if you already exited |
| Notion page picker cannot run | The terminal is non-interactive or Notion discovery failed | Rerun interactive `ktx setup`, or use non-interactive setup flags with explicit root page ids |
diff --git a/docs-site/content/docs/getting-started/quickstart.mdx b/docs-site/content/docs/getting-started/quickstart.mdx
index 35ec6009..abd6044d 100644
--- a/docs-site/content/docs/getting-started/quickstart.mdx
+++ b/docs-site/content/docs/getting-started/quickstart.mdx
@@ -295,6 +295,26 @@ Context sources:
dbt_main: memory update complete
```
+Before the build starts, **ktx** runs a live test for every connection the
+build depends on. A context build can take several minutes, so if any required
+connection is unreachable or misconfigured the build is blocked up front and
+**ktx** names the failing connection by id and connector type:
+
+```text
+KTX cannot build context: a required connection failed its live test.
+
+Failed connections:
+ warehouse (postgres)
+
+Each connection must be reachable before KTX builds context.
+Run `ktx connection test ` to see the error, fix the connection, then retry.
+```
+
+Run `ktx connection test ` to see the underlying error, fix the
+connection, then continue. In interactive setup you can retry without
+restarting; with `--no-input` the build exits non-zero and names the failing
+connection so scripts can stop early.
+
## Connect a coding agent
The setup wizard installs project-local agent rules in the last step. To
@@ -354,7 +374,8 @@ surface.
| `ktx: command not found` | Reinstall `@kaelio/ktx` and open a new shell |
| Setup resumes the wrong project | Pass `--project-dir ` |
| LLM or embeddings health check fails | Rerun setup and pick a different credential, model, or backend |
-| Database test fails | Verify the same connection with the database's native client, then rerun setup |
+| Database test fails | Use the setup recovery menu to retry or re-enter details; if it still fails, verify the same connection with the database's native client |
+| Context build blocked: a connection failed its live test | Run `ktx connection test ` to see the error, fix the connection, then retry the build |
| Agent integration is incomplete | Run `ktx setup --agents --target ` |
## Next steps
diff --git a/packages/cli/src/connection-recovery.ts b/packages/cli/src/connection-recovery.ts
new file mode 100644
index 00000000..2cd87448
--- /dev/null
+++ b/packages/cli/src/connection-recovery.ts
@@ -0,0 +1,132 @@
+import type { KtxCliIo } from './cli-runtime.js';
+import type { KtxSetupPromptOption } from './setup-prompts.js';
+
+export type RecoveryOutcome = 'ready' | 'skip' | 'back' | 'failed';
+
+/** @internal */
+export interface RecoveryAction {
+ value: string;
+ label: string;
+ run: () => Promise;
+}
+
+export type ConfigureResult = 'configured' | 'back' | 'cancelled';
+
+export type ValidateResult =
+ | { status: 'ok' }
+ | { status: 'back' }
+ | { status: 'failed'; extraActions?: RecoveryAction[] };
+
+export interface ConnectionRecoveryInput {
+ label: string;
+ interactive: boolean;
+ allowSkip: boolean;
+ io: KtxCliIo;
+ prompts: {
+ select(options: { message: string; options: KtxSetupPromptOption[] }): Promise;
+ };
+ snapshot: () => Promise<() => Promise>;
+ configure: () => Promise;
+ validate: () => Promise;
+}
+
+async function runRollbackOnce(input: {
+ rollback: () => Promise;
+ state: { rolledBack: boolean };
+}): Promise {
+ if (input.state.rolledBack) {
+ return;
+ }
+ input.state.rolledBack = true;
+ await input.rollback();
+}
+
+function recoveryOptions(input: {
+ allowSkip: boolean;
+ extraActions?: RecoveryAction[];
+}): KtxSetupPromptOption[] {
+ return [
+ { value: 'retry', label: 'Retry connection test' },
+ { value: 're-enter', label: 'Re-enter connection details' },
+ ...(input.extraActions ?? []).map((action) => ({
+ value: action.value,
+ label: action.label,
+ })),
+ ...(input.allowSkip ? [{ value: 'skip', label: 'Skip this connection' }] : []),
+ { value: 'back', label: 'Back' },
+ ];
+}
+
+export async function runConnectionSetupWithRecovery(
+ input: ConnectionRecoveryInput,
+): Promise {
+ const rollback = await input.snapshot();
+ const rollbackState = { rolledBack: false };
+
+ const firstConfig = await input.configure();
+ if (firstConfig === 'back') {
+ await runRollbackOnce({ rollback, state: rollbackState });
+ return 'back';
+ }
+ if (firstConfig === 'cancelled') {
+ await runRollbackOnce({ rollback, state: rollbackState });
+ return 'failed';
+ }
+
+ let validation = await input.validate();
+ while (validation.status !== 'ok') {
+ if (validation.status === 'back') {
+ await runRollbackOnce({ rollback, state: rollbackState });
+ return 'back';
+ }
+
+ if (!input.interactive) {
+ return 'failed';
+ }
+
+ const action = await input.prompts.select({
+ message: `Connection setup failed for ${input.label}`,
+ options: recoveryOptions({
+ allowSkip: input.allowSkip,
+ extraActions: validation.extraActions,
+ }),
+ });
+
+ if (action === 'back') {
+ await runRollbackOnce({ rollback, state: rollbackState });
+ return 'back';
+ }
+ if (action === 'skip' && input.allowSkip) {
+ await runRollbackOnce({ rollback, state: rollbackState });
+ return 'skip';
+ }
+ if (action === 're-enter') {
+ const nextConfig = await input.configure();
+ if (nextConfig === 'back') {
+ await runRollbackOnce({ rollback, state: rollbackState });
+ return 'back';
+ }
+ if (nextConfig === 'cancelled') {
+ await runRollbackOnce({ rollback, state: rollbackState });
+ return 'failed';
+ }
+ validation = await input.validate();
+ continue;
+ }
+ if (action === 'retry') {
+ validation = await input.validate();
+ continue;
+ }
+
+ const extraAction = validation.extraActions?.find((candidate) => candidate.value === action);
+ if (extraAction) {
+ await extraAction.run();
+ validation = await input.validate();
+ continue;
+ }
+
+ validation = await input.validate();
+ }
+
+ return 'ready';
+}
diff --git a/packages/cli/src/io/buffered-command-io.ts b/packages/cli/src/io/buffered-command-io.ts
new file mode 100644
index 00000000..6d16f385
--- /dev/null
+++ b/packages/cli/src/io/buffered-command-io.ts
@@ -0,0 +1,35 @@
+import type { KtxCliIo } from '../cli-runtime.js';
+
+export interface BufferedCommandIo extends KtxCliIo {
+ stdoutText(): string;
+ stderrText(): string;
+}
+
+/**
+ * Captures stdout/stderr from a command (e.g. `runKtxConnection`) into buffers
+ * instead of the terminal. Callers decide whether to flush the captured text to
+ * the user or discard it.
+ */
+export function createBufferedCommandIo(): BufferedCommandIo {
+ let stdout = '';
+ let stderr = '';
+ return {
+ stdout: {
+ isTTY: false,
+ write(chunk: string) {
+ stdout += chunk;
+ },
+ },
+ stderr: {
+ write(chunk: string) {
+ stderr += chunk;
+ },
+ },
+ stdoutText() {
+ return stdout;
+ },
+ stderrText() {
+ return stderr;
+ },
+ };
+}
diff --git a/packages/cli/src/setup-context.ts b/packages/cli/src/setup-context.ts
index 721c09bd..be458d2a 100644
--- a/packages/cli/src/setup-context.ts
+++ b/packages/cli/src/setup-context.ts
@@ -8,6 +8,8 @@ import type { KtxCliIo } from './cli-runtime.js';
import { errorMessage, writePrefixedLines } from './clack.js';
import { formatErrorDetail } from './telemetry/scrubber.js';
import { buildPublicIngestPlan } from './public-ingest.js';
+import { runKtxConnection } from './connection.js';
+import { type BufferedCommandIo, createBufferedCommandIo } from './io/buffered-command-io.js';
import type { KtxManagedPythonInstallPolicy } from './managed-python-command.js';
import {
type ContextBuildSourceProgressUpdate,
@@ -91,6 +93,7 @@ export interface KtxSetupContextDeps {
now?: () => Date;
runContextBuild?: typeof runContextBuild;
verifyContextReady?: (projectDir: string) => Promise;
+ testConnection?: (projectDir: string, connectionId: string, io: KtxCliIo) => Promise;
}
interface KtxSetupContextTargets {
@@ -277,6 +280,140 @@ function listContextTargets(project: KtxLocalProject): KtxSetupContextTargets {
};
}
+interface ConnectionGateFailure {
+ connectionId: string;
+ driver: string;
+}
+
+type ConnectionGateResult = { ok: true } | { ok: false; failures: ConnectionGateFailure[] };
+
+type PreparedBuild =
+ | { kind: 'ready'; project: KtxLocalProject; targets: KtxSetupContextTargets }
+ | { kind: 'result'; result: KtxSetupContextResult };
+
+function requiredConnectionIds(targets: KtxSetupContextTargets): string[] {
+ return [...targets.primarySourceConnectionIds, ...targets.contextSourceConnectionIds];
+}
+
+function connectorTypeLabel(project: KtxLocalProject, connectionId: string): string {
+ const driver = String(project.config.connections[connectionId]?.driver ?? '')
+ .trim()
+ .toLowerCase();
+ return driver.length > 0 ? driver : 'unknown';
+}
+
+async function defaultGateTestConnection(
+ projectDir: string,
+ connectionId: string,
+ io: KtxCliIo,
+): Promise {
+ return await runKtxConnection({ command: 'test', projectDir, connectionId }, io);
+}
+
+/**
+ * Runs a live connection test for every connection the build depends on. Each
+ * test's output is captured in a buffer and discarded so raw error text never
+ * reaches the user — callers surface only the connection id and connector type.
+ */
+async function testRequiredConnections(
+ projectDir: string,
+ project: KtxLocalProject,
+ targets: KtxSetupContextTargets,
+ testConnection: (projectDir: string, connectionId: string, io: KtxCliIo) => Promise,
+): Promise {
+ const failures: ConnectionGateFailure[] = [];
+ for (const connectionId of requiredConnectionIds(targets)) {
+ const buffered: BufferedCommandIo = createBufferedCommandIo();
+ const exitCode = await testConnection(projectDir, connectionId, buffered);
+ if (exitCode !== 0) {
+ failures.push({ connectionId, driver: connectorTypeLabel(project, connectionId) });
+ }
+ }
+ return failures.length === 0 ? { ok: true } : { ok: false, failures };
+}
+
+/**
+ * Loads the project and resolves the connections the build depends on, applying
+ * the empty-targets and preflight-capability checks. Used both on first entry
+ * and on interactive retry so a fix that adds, removes, or reconfigures a
+ * connection is honored.
+ */
+async function prepareBuildTargets(args: KtxSetupContextStepArgs, io: KtxCliIo): Promise {
+ const project = await loadKtxProject({ projectDir: args.projectDir });
+ const targets = listContextTargets(project);
+ if (targets.primarySourceConnectionIds.length === 0 && targets.contextSourceConnectionIds.length === 0) {
+ if (args.allowEmpty === true) {
+ return { kind: 'result', result: { status: 'skipped', projectDir: args.projectDir } };
+ }
+ io.stderr.write('No databases or context sources are configured for a KTX context build.\n');
+ return { kind: 'result', result: { status: 'failed', projectDir: args.projectDir } };
+ }
+ const preflightPlan = buildPublicIngestPlan(project, { projectDir: project.projectDir, all: true });
+ const preflightFailures = preflightPlan.targets.flatMap((target) =>
+ target.preflightFailure ? [`${target.connectionId}: ${target.preflightFailure}`] : [],
+ );
+ if (preflightFailures.length > 0) {
+ if (args.allowEmpty === true) {
+ return { kind: 'result', result: { status: 'skipped', projectDir: args.projectDir } };
+ }
+ writeMissingCapabilities(preflightFailures, io);
+ return { kind: 'result', result: { status: 'missing-input', projectDir: args.projectDir } };
+ }
+ return { kind: 'ready', project, targets };
+}
+
+function writeConnectionGateFailureLines(
+ io: KtxCliIo,
+ projectDir: string,
+ failures: ConnectionGateFailure[],
+): void {
+ io.stderr.write('KTX cannot build context: a required connection failed its live test.\n\n');
+ io.stderr.write('Failed connections:\n');
+ for (const failure of failures) {
+ io.stderr.write(` ${failure.connectionId} (${failure.driver})\n`);
+ }
+ io.stderr.write('\nEach connection must be reachable before KTX builds context.\n');
+ io.stderr.write(
+ `Run \`ktx connection test --project-dir ${resolve(projectDir)}\` to see the error, fix the connection, then retry.\n`,
+ );
+}
+
+function connectionGateFailureReason(failures: ConnectionGateFailure[]): string {
+ const names = failures.map((failure) => `${failure.connectionId} (${failure.driver})`).join(', ');
+ return `Required connections failed their live test: ${names}.`;
+}
+
+async function writeConnectionGateFailedState(
+ args: KtxSetupContextStepArgs,
+ deps: KtxSetupContextDeps,
+ targets: KtxSetupContextTargets,
+ failures: ConnectionGateFailure[],
+): Promise {
+ const at = (deps.now ?? (() => new Date()))().toISOString();
+ await writeKtxSetupContextState(args.projectDir, {
+ status: 'failed',
+ startedAt: at,
+ updatedAt: at,
+ primarySourceConnectionIds: targets.primarySourceConnectionIds,
+ contextSourceConnectionIds: targets.contextSourceConnectionIds,
+ reportIds: [],
+ artifactPaths: [],
+ retryableFailedTargets: [],
+ commands: contextBuildCommands(args.projectDir),
+ failureReason: connectionGateFailureReason(failures),
+ });
+}
+
+async function promptConnectionGateRetry(prompts: KtxSetupContextPromptAdapter): Promise<'retry' | 'back'> {
+ return (await prompts.select({
+ message: 'Fix the failing connection, then choose how to proceed.',
+ options: [
+ { value: 'retry', label: 'Retry connection tests' },
+ { value: 'back', label: 'Back' },
+ ],
+ })) as 'retry' | 'back';
+}
+
async function hasFileWithExtension(
root: string,
extensions: Set,
@@ -641,7 +778,6 @@ export async function runKtxSetupContextStep(
deps: KtxSetupContextDeps = {},
): Promise {
try {
- const project = await loadKtxProject({ projectDir: args.projectDir });
const prompts = deps.prompts ?? createPromptAdapter();
const existingState = await readKtxSetupContextState(args.projectDir);
const completedSteps = (await readKtxSetupState(args.projectDir)).completed_steps;
@@ -659,26 +795,12 @@ export async function runKtxSetupContextStep(
io.stdout.write('Previous context build state is stale; starting a fresh foreground build.\n');
}
- const targets = listContextTargets(project);
- if (targets.primarySourceConnectionIds.length === 0 && targets.contextSourceConnectionIds.length === 0) {
- if (args.allowEmpty === true) {
- return { status: 'skipped', projectDir: args.projectDir };
- }
- io.stderr.write('No databases or context sources are configured for a KTX context build.\n');
- return { status: 'failed', projectDir: args.projectDir };
- }
-
- const preflightPlan = buildPublicIngestPlan(project, { projectDir: project.projectDir, all: true });
- const preflightFailures = preflightPlan.targets.flatMap((target) =>
- target.preflightFailure ? [`${target.connectionId}: ${target.preflightFailure}`] : [],
- );
- if (preflightFailures.length > 0) {
- if (args.allowEmpty === true) {
- return { status: 'skipped', projectDir: args.projectDir };
- }
- writeMissingCapabilities(preflightFailures, io);
- return { status: 'missing-input', projectDir: args.projectDir };
+ const prepared = await prepareBuildTargets(args, io);
+ if (prepared.kind === 'result') {
+ return prepared.result;
}
+ let { project, targets } = prepared;
+ const interactive = args.inputMode !== 'disabled' && args.prompt !== false;
if (args.forcePrompt !== true && args.prompt !== false && deps.verifyContextReady === undefined) {
const existingContextResult = await completeExistingContext(args, io, deps, targets);
@@ -687,7 +809,7 @@ export async function runKtxSetupContextStep(
}
}
- if (args.inputMode !== 'disabled' && args.prompt !== false) {
+ if (interactive) {
const choice = await promptForBuild(prompts);
if (choice === 'back') {
return { status: 'back', projectDir: args.projectDir };
@@ -698,7 +820,32 @@ export async function runKtxSetupContextStep(
}
}
- return await runBuild(args, io, deps, project, targets);
+ // Live-connection gate: every connection the build depends on must pass a
+ // live test before the (expensive) build starts. A red connection is a hard
+ // stop — we surface only the connection id and connector type, never raw
+ // error text.
+ const testConnection = deps.testConnection ?? defaultGateTestConnection;
+ while (true) {
+ const gate = await testRequiredConnections(args.projectDir, project, targets, testConnection);
+ if (gate.ok) {
+ return await runBuild(args, io, deps, project, targets);
+ }
+ writeConnectionGateFailureLines(io, args.projectDir, gate.failures);
+ if (!interactive) {
+ await writeConnectionGateFailedState(args, deps, targets, gate.failures);
+ return { status: 'failed', projectDir: args.projectDir };
+ }
+ const choice = await promptConnectionGateRetry(prompts);
+ if (choice === 'back') {
+ return { status: 'back', projectDir: args.projectDir };
+ }
+ const reprepared = await prepareBuildTargets(args, io);
+ if (reprepared.kind === 'result') {
+ return reprepared.result;
+ }
+ project = reprepared.project;
+ targets = reprepared.targets;
+ }
} catch (error) {
writePrefixedLines((chunk) => io.stderr.write(chunk), errorMessage(error));
return { status: 'failed', projectDir: args.projectDir, errorDetail: formatErrorDetail(error) };
diff --git a/packages/cli/src/setup-databases.ts b/packages/cli/src/setup-databases.ts
index c2417031..1fd93486 100644
--- a/packages/cli/src/setup-databases.ts
+++ b/packages/cli/src/setup-databases.ts
@@ -22,6 +22,13 @@ import {
writePrefixedLines,
} from './clack.js';
import { runKtxConnection } from './connection.js';
+import { createBufferedCommandIo } from './io/buffered-command-io.js';
+import {
+ runConnectionSetupWithRecovery,
+ type ConfigureResult,
+ type RecoveryOutcome,
+ type ValidateResult,
+} from './connection-recovery.js';
import {
pickDatabaseScope as defaultPickDatabaseScope,
type DatabaseScopePickResult,
@@ -227,7 +234,6 @@ const SCOPE_DISCOVERY_SPECS: Partial;
-type ConnectionSetupStatus = 'ready' | 'back' | 'failed' | 'failed-query-history-unavailable';
const DRIVER_CONNECTION_DEFAULTS: Record = {
postgres: { port: '5432' },
@@ -994,35 +1000,6 @@ async function defaultScanConnection(projectDir: string, connectionId: string, i
);
}
-interface BufferedCommandIo extends KtxCliIo {
- stdoutText(): string;
- stderrText(): string;
-}
-
-function createBufferedCommandIo(): BufferedCommandIo {
- let stdout = '';
- let stderr = '';
- return {
- stdout: {
- isTTY: false,
- write(chunk: string) {
- stdout += chunk;
- },
- },
- stderr: {
- write(chunk: string) {
- stderr += chunk;
- },
- },
- stdoutText() {
- return stdout;
- },
- stderrText() {
- return stderr;
- },
- };
-}
-
function envWithCurrentNodeFirst(env: NodeJS.ProcessEnv = process.env): NodeJS.ProcessEnv {
return {
...env,
@@ -1203,6 +1180,31 @@ async function disableConnectionQueryHistory(projectDir: string, connectionId: s
});
}
+function okValidateResult(): ValidateResult {
+ return { status: 'ok' };
+}
+
+function backValidateResult(): ValidateResult {
+ return { status: 'back' };
+}
+
+function failedValidateResult(): ValidateResult {
+ return { status: 'failed' };
+}
+
+function queryHistoryUnavailableResult(projectDir: string, connectionId: string): ValidateResult {
+ return {
+ status: 'failed',
+ extraActions: [
+ {
+ value: 'disable-query-history',
+ label: 'Disable query history and retry',
+ run: () => disableConnectionQueryHistory(projectDir, connectionId),
+ },
+ ],
+ };
+}
+
async function createConnectionConfigRollback(projectDir: string, connectionId: string): Promise<() => Promise> {
const project = await loadKtxProject({ projectDir });
const previousConnection = project.config.connections[connectionId];
@@ -1330,11 +1332,11 @@ async function maybeConfigureDatabaseScope(input: {
io: KtxCliIo;
prompts: KtxSetupDatabasesPromptAdapter;
forcePrompt?: boolean;
-}): Promise {
+}): Promise {
const project = await loadKtxProject({ projectDir: input.projectDir });
const connection = project.config.connections[input.connectionId];
const driver = normalizeDriver(connection?.driver);
- if (!driver || driver === 'sqlite') return 'ready';
+ if (!driver || driver === 'sqlite') return okValidateResult();
const spec = SCOPE_DISCOVERY_SPECS[driver];
const existingTables = connection?.enabled_tables;
@@ -1343,7 +1345,7 @@ async function maybeConfigureDatabaseScope(input: {
const hasExistingScope = !spec || existingScope.length > 0;
if (hasExistingTables && hasExistingScope && input.forcePrompt !== true) {
- return 'ready';
+ return okValidateResult();
}
const cliSchemas = input.args.databaseSchemas;
@@ -1361,7 +1363,7 @@ async function maybeConfigureDatabaseScope(input: {
input.io.stderr.write(
`Could not discover ${spec.promptLabel.toLowerCase()} for ${input.connectionId}; ${detail}\n`,
);
- return 'ready';
+ return okValidateResult();
}
}
if (scopeToWrite.length > 0) {
@@ -1377,7 +1379,7 @@ async function maybeConfigureDatabaseScope(input: {
]);
}
}
- return 'ready';
+ return okValidateResult();
}
if (spec && cliSchemas.length > 0) {
@@ -1413,7 +1415,7 @@ async function maybeConfigureDatabaseScope(input: {
connectionId: input.connectionId,
spec,
});
- if (typed === undefined) return 'back';
+ if (typed === undefined) return backValidateResult();
effectiveCliSchemas = typed;
listedSchemas = typed;
if (typed.length > 0) {
@@ -1428,7 +1430,7 @@ async function maybeConfigureDatabaseScope(input: {
}
const schemas = unique(listedSchemas);
if (spec && schemas.length === 0) {
- return 'ready';
+ return okValidateResult();
}
const schemaSuggestion =
effectiveCliSchemas.length > 0
@@ -1465,10 +1467,10 @@ async function maybeConfigureDatabaseScope(input: {
? `Could not discover tables for ${input.connectionId}; edit was not saved. ${detail}`
: `Could not discover tables for ${input.connectionId}; continuing without table filter. ${detail}`,
);
- return input.forcePrompt === true ? 'failed' : 'ready';
+ return input.forcePrompt === true ? failedValidateResult() : okValidateResult();
}
if (pickResult.kind === 'back') {
- return 'back';
+ return backValidateResult();
}
const enabledTables = pickResult.enabledTables;
const activeSchemas = pickResult.activeSchemas;
@@ -1483,7 +1485,7 @@ async function maybeConfigureDatabaseScope(input: {
}
const refreshedProject = await loadKtxProject({ projectDir: input.projectDir });
const currentConnection = refreshedProject.config.connections[input.connectionId];
- if (!currentConnection) return 'ready';
+ if (!currentConnection) return okValidateResult();
await writeConnectionConfig({
projectDir: input.projectDir,
connectionId: input.connectionId,
@@ -1500,7 +1502,7 @@ async function maybeConfigureDatabaseScope(input: {
writeSetupSection(input.io, `Tables enabled for ${input.connectionId}`, [
`✓ ${enabledTables.length} tables enabled`,
]);
- return 'ready';
+ return okValidateResult();
}
async function ensureHistoricSqlIngestDefaults(projectDir: string): Promise {
@@ -1628,7 +1630,7 @@ async function validateAndScanConnection(input: {
args: KtxSetupDatabasesArgs;
prompts: KtxSetupDatabasesPromptAdapter;
forceScopeAndTables?: boolean;
-}): Promise {
+}): Promise {
const testConnection = input.deps.testConnection ?? defaultTestConnection;
const scanConnection = input.deps.scanConnection ?? defaultScanConnection;
const project = await loadKtxProject({ projectDir: input.projectDir });
@@ -1642,7 +1644,7 @@ async function validateAndScanConnection(input: {
(chunk) => input.io.stderr.write(chunk),
`Connection test failed for ${input.connectionId}.`,
);
- return 'failed';
+ return failedValidateResult();
}
const testOutput = testIo.stdoutText();
const outputDriver = normalizeDriver(readOutputValue(testOutput, 'Driver'));
@@ -1651,7 +1653,7 @@ async function validateAndScanConnection(input: {
writeSetupSection(input.io, `Testing ${input.connectionId}`, testLines);
const scopeStatus = await maybeConfigureDatabaseScope({ ...input, forcePrompt: input.forceScopeAndTables });
- if (scopeStatus !== 'ready') {
+ if (scopeStatus.status !== 'ok') {
return scopeStatus;
}
@@ -1712,7 +1714,9 @@ async function validateAndScanConnection(input: {
);
}
if (scanCode !== 0) {
- return queryHistoryAvailable ? 'failed' : 'failed-query-history-unavailable';
+ return queryHistoryAvailable
+ ? failedValidateResult()
+ : queryHistoryUnavailableResult(input.projectDir, input.connectionId);
}
}
const scanOutput = scanIo.stdoutText();
@@ -1724,7 +1728,7 @@ async function validateAndScanConnection(input: {
writeSetupSection(input.io, 'Database ready', [
`${input.connectionId} · ${driverDisplay} · schema context complete`,
]);
- return 'ready';
+ return okValidateResult();
}
async function chooseDrivers(
@@ -1847,6 +1851,149 @@ async function choosePrimarySourceToEdit(input: {
return choice === 'back' ? 'back' : choice;
}
+async function configureDatabaseConnection(input: {
+ projectDir: string;
+ connectionId: string;
+ driver: KtxSetupDatabaseDriver;
+ args: KtxSetupDatabasesArgs;
+ prompts: KtxSetupDatabasesPromptAdapter;
+ io: KtxCliIo;
+ canReturnToDriverSelection: boolean;
+ editBaseline?: KtxProjectConnectionConfig;
+}): Promise {
+ const project = await loadKtxProject({ projectDir: input.projectDir });
+ const latestConnection = project.config.connections[input.connectionId];
+ let connection = await buildConnectionConfig({
+ driver: input.driver,
+ connectionId: input.connectionId,
+ args: input.args,
+ prompts: input.prompts,
+ existingConnection: latestConnection,
+ });
+
+ while (!connection && input.args.inputMode !== 'disabled') {
+ const action = await input.prompts.select(
+ missingConnectionDetailsPrompt(driverLabel(input.driver), input.canReturnToDriverSelection),
+ );
+ if (action === 'back') {
+ return 'back';
+ }
+ connection = await buildConnectionConfig({
+ driver: input.driver,
+ connectionId: input.connectionId,
+ args: input.args,
+ prompts: input.prompts,
+ existingConnection: latestConnection,
+ });
+ }
+
+ if (connection === 'back') {
+ return 'back';
+ }
+ if (!connection) {
+ input.io.stderr.write(`Missing connection details for ${driverLabel(input.driver)}.\n`);
+ return 'cancelled';
+ }
+
+ const withHistoricSql = await maybeApplyHistoricSqlConfig({
+ connection,
+ driver: input.driver,
+ args: input.args,
+ prompts: input.prompts,
+ });
+ if (withHistoricSql === 'back') {
+ return 'back';
+ }
+
+ await writeConnectionConfig({
+ projectDir: input.projectDir,
+ connectionId: input.connectionId,
+ connection: input.editBaseline
+ ? withExistingPrimaryEditPromptDefaults({
+ previous: input.editBaseline,
+ next: withHistoricSql,
+ driver: input.driver,
+ })
+ : withHistoricSql,
+ io: input.io,
+ });
+ return 'configured';
+}
+
+async function runDatabaseConnectionSetupWithRecovery(input: {
+ projectDir: string;
+ connectionId: string;
+ driver: KtxSetupDatabaseDriver;
+ args: KtxSetupDatabasesArgs;
+ prompts: KtxSetupDatabasesPromptAdapter;
+ io: KtxCliIo;
+ deps: KtxSetupDatabasesDeps;
+ canReturnToDriverSelection: boolean;
+ allowSkip: boolean;
+ interactive?: boolean;
+ forceScopeAndTables?: boolean;
+ editBaseline?: KtxProjectConnectionConfig;
+ reuseExistingOnFirstConfigure?: boolean;
+}): Promise {
+ let configureCalls = 0;
+ // `configureDatabaseConnection` returns 'cancelled' only when required
+ // connection details are absent in non-interactive mode. The recovery
+ // primitive collapses that into 'failed', so we track it here to restore the
+ // distinct 'missing-input' outcome the surrounding step reports for
+ // incomplete flags (vs. a real connection/probe failure).
+ let sawMissingInput = false;
+
+ const outcome = await runConnectionSetupWithRecovery({
+ label: input.connectionId,
+ interactive: input.interactive ?? input.args.inputMode !== 'disabled',
+ allowSkip: input.allowSkip,
+ io: input.io,
+ prompts: input.prompts,
+ snapshot: () => createConnectionConfigRollback(input.projectDir, input.connectionId),
+ configure: async () => {
+ configureCalls += 1;
+ if (input.reuseExistingOnFirstConfigure && configureCalls === 1) {
+ const historicSqlResult = await applyHistoricSqlConfigToExistingConnection({
+ projectDir: input.projectDir,
+ connectionId: input.connectionId,
+ args: input.args,
+ prompts: input.prompts,
+ });
+ return historicSqlResult === 'back' ? 'back' : 'configured';
+ }
+ const configured = await configureDatabaseConnection({
+ projectDir: input.projectDir,
+ connectionId: input.connectionId,
+ driver: input.driver,
+ args: input.args,
+ prompts: input.prompts,
+ io: input.io,
+ canReturnToDriverSelection: input.canReturnToDriverSelection,
+ editBaseline: input.editBaseline,
+ });
+ if (configured === 'cancelled') {
+ sawMissingInput = true;
+ }
+ return configured;
+ },
+ validate: () =>
+ validateAndScanConnection({
+ projectDir: input.projectDir,
+ connectionId: input.connectionId,
+ io: input.io,
+ deps: input.deps,
+ args: input.args,
+ prompts: input.prompts,
+ forceScopeAndTables: input.forceScopeAndTables,
+ }),
+ });
+
+ if (outcome === 'failed' && sawMissingInput) {
+ return 'missing-input';
+ }
+ return outcome;
+}
+
async function runPrimarySourceFullEdit(input: {
projectDir: string;
connectionId: string;
@@ -1854,7 +2001,7 @@ async function runPrimarySourceFullEdit(input: {
prompts: KtxSetupDatabasesPromptAdapter;
io: KtxCliIo;
deps: KtxSetupDatabasesDeps;
-}): Promise<'ready' | 'back' | 'failed'> {
+}): Promise<'ready' | 'back' | 'failed' | 'missing-input'> {
const project = await loadKtxProject({ projectDir: input.projectDir });
const existing = project.config.connections[input.connectionId];
const driver = normalizeDriver(existing?.driver);
@@ -1866,59 +2013,21 @@ async function runPrimarySourceFullEdit(input: {
return 'failed';
}
- const rollback = await createConnectionConfigRollback(input.projectDir, input.connectionId);
- const replacement = await buildConnectionConfig({
- driver,
+ const outcome = await runDatabaseConnectionSetupWithRecovery({
+ projectDir: input.projectDir,
connectionId: input.connectionId,
- args: input.args,
- prompts: input.prompts,
- existingConnection: existing,
- });
- if (replacement === 'back') {
- await rollback();
- return 'back';
- }
- if (!replacement) {
- await rollback();
- return 'failed';
- }
-
- const withHistoricSql = await maybeApplyHistoricSqlConfig({
- connection: replacement,
driver,
args: input.args,
prompts: input.prompts,
- });
- if (withHistoricSql === 'back') {
- await rollback();
- return 'back';
- }
-
- await writeConnectionConfig({
- projectDir: input.projectDir,
- connectionId: input.connectionId,
- connection: withExistingPrimaryEditPromptDefaults({
- previous: existing,
- next: withHistoricSql,
- driver,
- }),
- io: input.io,
- });
-
- const validated = await validateAndScanConnection({
- projectDir: input.projectDir,
- connectionId: input.connectionId,
io: input.io,
deps: input.deps,
- args: input.args,
- prompts: input.prompts,
+ canReturnToDriverSelection: true,
+ allowSkip: false,
forceScopeAndTables: true,
+ editBaseline: existing,
});
- if (validated !== 'ready') {
- await rollback();
- return validated === 'failed-query-history-unavailable' ? 'failed' : validated;
- }
- return 'ready';
+
+ return outcome === 'skip' ? 'back' : outcome;
}
export async function runKtxSetupDatabasesStep(
@@ -1936,28 +2045,37 @@ export async function runKtxSetupDatabasesStep(
if (args.databaseConnectionIds && args.databaseConnectionIds.length > 0) {
const selectedConnectionIds: string[] = [];
for (const connectionId of unique(args.databaseConnectionIds)) {
- const historicSqlResult = await applyHistoricSqlConfigToExistingConnection({
- projectDir: args.projectDir,
- connectionId,
- args,
- prompts,
- });
- if (historicSqlResult === 'back') return { status: 'back', projectDir: args.projectDir };
- const setupStatus = await validateAndScanConnection({
- projectDir: args.projectDir,
- connectionId,
- io,
- deps,
- args,
- prompts,
- });
- if (setupStatus === 'back') {
- return { status: 'back', projectDir: args.projectDir };
- }
- if (setupStatus === 'failed') {
+ const project = await loadKtxProject({ projectDir: args.projectDir });
+ const driver = normalizeDriver(project.config.connections[connectionId]?.driver);
+ if (!driver) {
+ writePrefixedLines((chunk) => io.stderr.write(chunk), `Connection "${connectionId}" is not configured.`);
return { status: 'failed', projectDir: args.projectDir };
}
- selectedConnectionIds.push(connectionId);
+ const setupOutcome = await runDatabaseConnectionSetupWithRecovery({
+ projectDir: args.projectDir,
+ connectionId,
+ driver,
+ args,
+ prompts,
+ io,
+ deps,
+ canReturnToDriverSelection: false,
+ allowSkip: false,
+ interactive: false,
+ reuseExistingOnFirstConfigure: true,
+ });
+ if (setupOutcome === 'back') {
+ return { status: 'back', projectDir: args.projectDir };
+ }
+ if (setupOutcome === 'missing-input') {
+ return { status: 'missing-input', projectDir: args.projectDir };
+ }
+ if (setupOutcome === 'failed') {
+ return { status: 'failed', projectDir: args.projectDir };
+ }
+ if (setupOutcome === 'ready') {
+ selectedConnectionIds.push(connectionId);
+ }
}
await markDatabasesComplete(args.projectDir, selectedConnectionIds);
return { status: 'ready', projectDir: args.projectDir, connectionIds: selectedConnectionIds };
@@ -2009,6 +2127,9 @@ export async function runKtxSetupDatabasesStep(
showConfiguredPrimaryMenu = true;
continue;
}
+ if (editResult === 'missing-input') {
+ return { status: 'missing-input', projectDir: args.projectDir };
+ }
if (editResult === 'failed') {
return { status: 'failed', projectDir: args.projectDir };
}
@@ -2064,7 +2185,6 @@ export async function runKtxSetupDatabasesStep(
return { status: 'missing-input', projectDir: args.projectDir };
}
- let connectionAlreadyValidated = false;
if (connectionChoice.kind === 'edit') {
const editResult = await runPrimarySourceFullEdit({
projectDir: args.projectDir,
@@ -2079,176 +2199,41 @@ export async function runKtxSetupDatabasesStep(
returnToDriverSelection = true;
break;
}
+ if (editResult === 'missing-input') {
+ return { status: 'missing-input', projectDir: args.projectDir };
+ }
if (editResult === 'failed') {
return { status: 'failed', projectDir: args.projectDir };
}
- connectionAlreadyValidated = true;
- } else if (connectionChoice.kind === 'new') {
- let connection = await buildConnectionConfig({
- driver,
+ } else {
+ const setupOutcome = await runDatabaseConnectionSetupWithRecovery({
+ projectDir: args.projectDir,
connectionId: connectionChoice.connectionId,
+ driver,
args,
prompts,
+ io,
+ deps,
+ canReturnToDriverSelection,
+ allowSkip: true,
+ reuseExistingOnFirstConfigure: connectionChoice.kind === 'existing',
});
- if (connection === 'back') {
+ if (setupOutcome === 'back') {
if (!canReturnToDriverSelection) return { status: 'back', projectDir: args.projectDir };
returnToDriverSelection = true;
break;
}
- while (!connection && args.inputMode !== 'disabled') {
- const label = driverLabel(driver);
- const action = await prompts.select(missingConnectionDetailsPrompt(label, canReturnToDriverSelection));
- if (action === 'back') {
- if (!canReturnToDriverSelection) return { status: 'back', projectDir: args.projectDir };
- returnToDriverSelection = true;
- break;
- }
- connection = await buildConnectionConfig({
- driver,
- connectionId: connectionChoice.connectionId,
- args,
- prompts,
- });
- if (connection === 'back') {
- if (!canReturnToDriverSelection) return { status: 'back', projectDir: args.projectDir };
- returnToDriverSelection = true;
- break;
- }
- }
- if (returnToDriverSelection) {
- break;
- }
- if (connection === 'back') {
- break;
- }
- if (!connection) {
- io.stderr.write(`Missing connection details for ${driverLabel(driver)}.\n`);
+ if (setupOutcome === 'missing-input') {
return { status: 'missing-input', projectDir: args.projectDir };
}
- const withHistoricSql = await maybeApplyHistoricSqlConfig({ connection, driver, args, prompts });
- if (withHistoricSql === 'back') {
- if (!canReturnToDriverSelection) return { status: 'back', projectDir: args.projectDir };
- returnToDriverSelection = true;
- break;
+ if (setupOutcome === 'failed') {
+ return { status: 'failed', projectDir: args.projectDir };
}
- await writeConnectionConfig({
- projectDir: args.projectDir,
- connectionId: connectionChoice.connectionId,
- connection: withHistoricSql,
- io,
- });
- } else {
- const existing = project.config.connections[connectionChoice.connectionId];
- const withHistoricSql = await maybeApplyHistoricSqlConfig({ connection: existing, driver, args, prompts });
- if (withHistoricSql === 'back') {
- if (!canReturnToDriverSelection) return { status: 'back', projectDir: args.projectDir };
- returnToDriverSelection = true;
- break;
- }
- await writeConnectionConfig({
- projectDir: args.projectDir,
- connectionId: connectionChoice.connectionId,
- connection: withHistoricSql,
- io,
- });
- }
-
- let connectionSkipped = false;
- let setupStatus: ConnectionSetupStatus = connectionAlreadyValidated
- ? 'ready'
- : await validateAndScanConnection({
- projectDir: args.projectDir,
- connectionId: connectionChoice.connectionId,
- io,
- deps,
- args,
- prompts,
- });
- while (!connectionAlreadyValidated && setupStatus !== 'ready') {
- if (setupStatus === 'back') {
- if (!canReturnToDriverSelection) return { status: 'back', projectDir: args.projectDir };
- returnToDriverSelection = true;
- break;
- }
- if (args.inputMode === 'disabled') return { status: 'failed', projectDir: args.projectDir };
- const failureOptions = [
- { value: 'retry', label: 'Retry connection test' },
- { value: 're-enter', label: 'Re-enter connection details' },
- ...(setupStatus === 'failed-query-history-unavailable'
- ? [{ value: 'disable-query-history', label: 'Disable query history and retry' }]
- : []),
- { value: 'skip', label: 'Skip this database' },
- { value: 'back', label: 'Back' },
- ];
- const action = await prompts.select({
- message: `Database setup failed for ${connectionChoice.connectionId}`,
- options: failureOptions,
- });
- if (action === 'back') {
- if (!canReturnToDriverSelection) return { status: 'back', projectDir: args.projectDir };
- returnToDriverSelection = true;
- break;
- }
- if (action === 'skip') {
- connectionSkipped = true;
- break;
- }
- if (action === 'retry') {
- setupStatus = await validateAndScanConnection({
- projectDir: args.projectDir,
- connectionId: connectionChoice.connectionId,
- io,
- deps,
- args,
- prompts,
- });
- } else if (action === 'disable-query-history') {
- await disableConnectionQueryHistory(args.projectDir, connectionChoice.connectionId);
- setupStatus = await validateAndScanConnection({
- projectDir: args.projectDir,
- connectionId: connectionChoice.connectionId,
- io,
- deps,
- args,
- prompts,
- });
- } else if (action === 're-enter') {
- const connection = await buildConnectionConfig({
- driver,
- connectionId: connectionChoice.connectionId,
- args,
- prompts,
- });
- if (connection === 'back') {
- if (!canReturnToDriverSelection) return { status: 'back', projectDir: args.projectDir };
- returnToDriverSelection = true;
- break;
- }
- if (!connection) continue;
- const withHistoricSql = await maybeApplyHistoricSqlConfig({ connection, driver, args, prompts });
- if (withHistoricSql === 'back') {
- if (!canReturnToDriverSelection) return { status: 'back', projectDir: args.projectDir };
- returnToDriverSelection = true;
- break;
- }
- await writeConnectionConfig({
- projectDir: args.projectDir,
- connectionId: connectionChoice.connectionId,
- connection: withHistoricSql,
- io,
- });
- setupStatus = await validateAndScanConnection({
- projectDir: args.projectDir,
- connectionId: connectionChoice.connectionId,
- io,
- deps,
- args,
- prompts,
- });
+ if (setupOutcome === 'skip') {
+ continue;
}
}
if (returnToDriverSelection) break;
- if (connectionSkipped) continue;
pushUniqueConnectionId(selectedConnectionIds, connectionChoice.connectionId);
}
diff --git a/packages/cli/src/setup-sources.ts b/packages/cli/src/setup-sources.ts
index 4f0a94bc..0a66c3a7 100644
--- a/packages/cli/src/setup-sources.ts
+++ b/packages/cli/src/setup-sources.ts
@@ -20,6 +20,12 @@ import type { KtxCliIo } from './cli-runtime.js';
import { errorMessage, writePrefixedLines } from './clack.js';
import { pickNotionRootPages } from './notion-page-picker.js';
import { runKtxSourceMapping } from './source-mapping.js';
+import {
+ runConnectionSetupWithRecovery,
+ type ConfigureResult,
+ type RecoveryOutcome,
+ type ValidateResult,
+} from './connection-recovery.js';
import { withMultiselectNavigation, withTextInputNavigation } from './prompt-navigation.js';
import { runKtxPublicIngest } from './public-ingest.js';
import { writeProjectLocalSecretReference } from './setup-secrets.js';
@@ -866,8 +872,7 @@ type InteractiveSourceConnectionChoice =
type SourceSetupChoiceResult =
| { status: 'ready'; connectionId: string }
- | { status: 'back' }
- | { status: 'failed' };
+ | { status: Exclude };
async function runSourcePromptSteps(
initialState: SourcePromptState,
@@ -1758,6 +1763,58 @@ async function validateSource(
return await (deps.validateNotion ?? defaultValidateNotion)(args.connection);
}
+async function createSourceSetupRollback(projectDir: string): Promise<() => Promise> {
+ const project = await loadKtxProject({ projectDir });
+ const previousConfig = project.config;
+ const configPath = project.configPath;
+ return async () => {
+ await writeFile(configPath, serializeKtxProjectConfig(previousConfig), 'utf-8');
+ };
+}
+
+function sourceConnectionId(input: {
+ source: KtxSetupSourceType;
+ sourceChoice: Exclude;
+}): string {
+ return input.sourceChoice.kind === 'existing' || input.sourceChoice.kind === 'edited'
+ ? input.sourceChoice.connectionId
+ : (input.sourceChoice.args.sourceConnectionId ?? `${input.source}-main`);
+}
+
+async function validateSourceConnectionAndMapping(input: {
+ args: KtxSetupSourcesArgs;
+ source: KtxSetupSourceType;
+ connectionId: string;
+ connection: KtxProjectConnectionConfig;
+ prompts: KtxSetupSourcesPromptAdapter;
+ io: KtxCliIo;
+ deps: KtxSetupSourcesDeps;
+}): Promise {
+ const validation = await validateSource(
+ input.source,
+ { projectDir: input.args.projectDir, connectionId: input.connectionId, connection: input.connection },
+ input.deps,
+ );
+ if (!validation.ok) {
+ input.io.stderr.write(`${validation.message}\n`);
+ return { status: 'failed' };
+ }
+
+ if (input.source === 'metabase' || input.source === 'looker') {
+ input.prompts.log?.(`Validating ${sourceLabel(input.source)} mapping...`);
+ const mappingCode = await (input.deps.runMapping ?? defaultRunMapping)(
+ input.args.projectDir,
+ input.connectionId,
+ createSetupPrefixedIo(input.io),
+ );
+ if (mappingCode !== 0) {
+ return { status: 'failed' };
+ }
+ }
+
+ return { status: 'ok' };
+}
+
async function saveValidateAndMaybeBuildSource(input: {
args: KtxSetupSourcesArgs;
source: KtxSetupSourceType;
@@ -1766,76 +1823,121 @@ async function saveValidateAndMaybeBuildSource(input: {
io: KtxCliIo;
deps: KtxSetupSourcesDeps;
}): Promise {
- const connectionId =
- input.sourceChoice.kind === 'existing'
- ? input.sourceChoice.connectionId
- : input.sourceChoice.kind === 'edited'
- ? input.sourceChoice.connectionId
- : (input.sourceChoice.args.sourceConnectionId ?? `${input.source}-main`);
- const connection =
- input.sourceChoice.kind === 'existing'
- ? input.sourceChoice.connection
- : buildConnection(input.source, input.sourceChoice.args);
- const rollback =
- input.sourceChoice.kind === 'existing'
- ? undefined
- : await writeSourceConnection(
- input.args.projectDir,
- connectionId,
- connection,
- sourceAdapter(input.source),
- input.io,
- );
+ let latestChoice = input.sourceChoice;
+ let latestConnectionId = sourceConnectionId({ source: input.source, sourceChoice: latestChoice });
+ let latestConnection =
+ latestChoice.kind === 'existing'
+ ? latestChoice.connection
+ : buildConnection(input.source, latestChoice.args);
+ let configureCount = 0;
+ let rollbackAfterConfigure: (() => Promise) | undefined;
- if (input.sourceChoice.kind === 'existing') {
- await ensureSourceAdapterEnabled(input.args.projectDir, input.source);
- }
+ const outcome = await runConnectionSetupWithRecovery({
+ label: latestConnectionId,
+ interactive: input.args.inputMode !== 'disabled',
+ allowSkip: true,
+ io: input.io,
+ prompts: input.prompts,
+ snapshot: async () => {
+ rollbackAfterConfigure = await createSourceSetupRollback(input.args.projectDir);
+ return rollbackAfterConfigure;
+ },
+ configure: async (): Promise => {
+ configureCount += 1;
+ if (latestChoice.kind === 'existing' && configureCount === 1) {
+ await ensureSourceAdapterEnabled(input.args.projectDir, input.source);
+ return 'configured';
+ }
- const validation = await validateSource(
- input.source,
- { projectDir: input.args.projectDir, connectionId, connection },
- input.deps,
- );
- if (!validation.ok) {
- await rollback?.();
- input.io.stderr.write(`${validation.message}\n`);
- return { status: 'failed' };
- }
+ const project = await loadKtxProject({ projectDir: input.args.projectDir });
+ const currentConnection = project.config.connections[latestConnectionId] ?? latestConnection;
+ const useAlreadyPromptedArgs = configureCount === 1 && latestChoice.kind !== 'existing';
+ const sourceArgs =
+ useAlreadyPromptedArgs && latestChoice.kind !== 'existing'
+ ? latestChoice.args
+ : input.args.inputMode === 'disabled'
+ ? sourceArgsFromExistingConnection({
+ args: input.args,
+ source: input.source,
+ connectionId: latestConnectionId,
+ connection: currentConnection,
+ })
+ : await promptForInteractiveSource(
+ sourceArgsFromExistingConnection({
+ args: input.args,
+ source: input.source,
+ connectionId: latestConnectionId,
+ connection: currentConnection,
+ }),
+ input.source,
+ input.prompts,
+ input.io,
+ {
+ pickNotionRootPages: input.deps.pickNotionRootPages,
+ discoverMetabaseDatabases: input.deps.discoverMetabaseDatabases,
+ },
+ latestConnectionId,
+ input.deps.testGitRepo,
+ input.deps.discoverMetabaseDatabases,
+ );
- if (input.source === 'metabase' || input.source === 'looker') {
- input.prompts.log?.(`Validating ${sourceLabel(input.source)} mapping…`);
- const mappingCode = await (input.deps.runMapping ?? defaultRunMapping)(
- input.args.projectDir,
- connectionId,
- createSetupPrefixedIo(input.io),
- );
- if (mappingCode !== 0) {
- await rollback?.();
- return { status: 'failed' };
- }
+ if (sourceArgs === 'back') {
+ return 'back';
+ }
+
+ latestConnectionId = sourceArgs.sourceConnectionId ?? latestConnectionId;
+ latestConnection = buildConnection(input.source, sourceArgs);
+ latestChoice =
+ latestChoice.kind === 'new'
+ ? { kind: 'new', args: sourceArgs }
+ : { kind: 'edited', connectionId: latestConnectionId, args: sourceArgs };
+
+ await writeSourceConnection(
+ input.args.projectDir,
+ latestConnectionId,
+ latestConnection,
+ sourceAdapter(input.source),
+ input.io,
+ );
+ return 'configured';
+ },
+ validate: () =>
+ validateSourceConnectionAndMapping({
+ args: input.args,
+ source: input.source,
+ connectionId: latestConnectionId,
+ connection: latestConnection,
+ prompts: input.prompts,
+ io: input.io,
+ deps: input.deps,
+ }),
+ });
+
+ if (outcome !== 'ready') {
+ return { status: outcome };
}
if (input.args.runInitialSourceIngest) {
const ingestResult = await runInitialSourceIngestWithRecovery({
args: input.args,
- connectionId,
+ connectionId: latestConnectionId,
io: input.io,
prompts: input.prompts,
deps: input.deps,
});
if (ingestResult === 'failed') {
- await rollback?.();
+ await rollbackAfterConfigure?.();
return { status: 'failed' };
}
if (ingestResult === 'back') {
- await rollback?.();
+ await rollbackAfterConfigure?.();
return { status: 'back' };
}
} else {
- input.io.stdout.write(`│ Context source ${connectionId} saved. It will be built during the context build step.\n`);
+ input.io.stdout.write(`│ Context source ${latestConnectionId} saved. It will be built during the context build step.\n`);
}
- return { status: 'ready', connectionId };
+ return { status: 'ready', connectionId: latestConnectionId };
}
export async function runKtxSetupSourcesStep(
@@ -1942,8 +2044,13 @@ export async function runKtxSetupSourcesStep(
returnToSourceSelection = true;
break;
}
- if (!readyConnectionIds.includes(choiceResult.connectionId)) {
- readyConnectionIds.push(choiceResult.connectionId);
+ if (choiceResult.status === 'skip') {
+ continue;
+ }
+ if (choiceResult.status === 'ready') {
+ if (!readyConnectionIds.includes(choiceResult.connectionId)) {
+ readyConnectionIds.push(choiceResult.connectionId);
+ }
}
}
@@ -2005,8 +2112,13 @@ export async function runKtxSetupSourcesStep(
if (choiceResult.status === 'back') {
continue;
}
- if (!readyConnectionIds.includes(choiceResult.connectionId)) {
- readyConnectionIds.push(choiceResult.connectionId);
+ if (choiceResult.status === 'skip') {
+ continue;
+ }
+ if (choiceResult.status === 'ready') {
+ if (!readyConnectionIds.includes(choiceResult.connectionId)) {
+ readyConnectionIds.push(choiceResult.connectionId);
+ }
}
continue;
}
diff --git a/packages/cli/test/connection-recovery.test.ts b/packages/cli/test/connection-recovery.test.ts
new file mode 100644
index 00000000..b164c7e2
--- /dev/null
+++ b/packages/cli/test/connection-recovery.test.ts
@@ -0,0 +1,171 @@
+import { describe, expect, it, vi } from 'vitest';
+import {
+ runConnectionSetupWithRecovery,
+ type ConfigureResult,
+ type RecoveryAction,
+ type ValidateResult,
+} from '../src/connection-recovery.js';
+
+function input(overrides: {
+ interactive?: boolean;
+ allowSkip?: boolean;
+ configure?: () => Promise;
+ validate?: () => Promise;
+ selectValues?: string[];
+ extraActions?: RecoveryAction[];
+}) {
+ const selectValues = [...(overrides.selectValues ?? [])];
+ const rollback = vi.fn(async () => {});
+ const select = vi.fn(async () => selectValues.shift() ?? 'back');
+ const validate = overrides.validate ?? vi.fn(async () => ({ status: 'ok' as const }));
+ return {
+ rollback,
+ select,
+ validate,
+ run: () =>
+ runConnectionSetupWithRecovery({
+ label: 'warehouse',
+ interactive: overrides.interactive ?? true,
+ allowSkip: overrides.allowSkip ?? true,
+ io: {
+ stdout: { write: vi.fn() },
+ stderr: { write: vi.fn() },
+ },
+ prompts: { select },
+ snapshot: vi.fn(async () => rollback),
+ configure: overrides.configure ?? vi.fn(async () => 'configured' as const),
+ validate,
+ }),
+ };
+}
+
+describe('runConnectionSetupWithRecovery', () => {
+ it('returns ready without opening the menu when first validation passes', async () => {
+ const setup = input({});
+
+ await expect(setup.run()).resolves.toBe('ready');
+
+ expect(setup.select).not.toHaveBeenCalled();
+ expect(setup.rollback).not.toHaveBeenCalled();
+ });
+
+ it('fails fast without prompting or rollback when noninteractive validation fails', async () => {
+ const setup = input({
+ interactive: false,
+ validate: vi.fn(async () => ({ status: 'failed' as const })),
+ });
+
+ await expect(setup.run()).resolves.toBe('failed');
+
+ expect(setup.select).not.toHaveBeenCalled();
+ expect(setup.rollback).not.toHaveBeenCalled();
+ });
+
+ it('retries the same config after Retry and returns ready', async () => {
+ let calls = 0;
+ const setup = input({
+ selectValues: ['retry'],
+ validate: vi.fn(async () => {
+ calls += 1;
+ return calls === 1 ? { status: 'failed' as const } : { status: 'ok' as const };
+ }),
+ });
+
+ await expect(setup.run()).resolves.toBe('ready');
+
+ expect(setup.validate).toHaveBeenCalledTimes(2);
+ expect(setup.rollback).not.toHaveBeenCalled();
+ });
+
+ it('re-enters config and validates the new attempt', async () => {
+ let calls = 0;
+ const configure = vi.fn(async () => 'configured' as const);
+ const setup = input({
+ configure,
+ selectValues: ['re-enter'],
+ validate: vi.fn(async () => {
+ calls += 1;
+ return calls === 1 ? { status: 'failed' as const } : { status: 'ok' as const };
+ }),
+ });
+
+ await expect(setup.run()).resolves.toBe('ready');
+
+ expect(configure).toHaveBeenCalledTimes(2);
+ expect(setup.validate).toHaveBeenCalledTimes(2);
+ expect(setup.rollback).not.toHaveBeenCalled();
+ });
+
+ it('rolls back once and returns skip when Skip is selected', async () => {
+ const setup = input({
+ selectValues: ['skip'],
+ validate: vi.fn(async () => ({ status: 'failed' as const })),
+ });
+
+ await expect(setup.run()).resolves.toBe('skip');
+
+ expect(setup.rollback).toHaveBeenCalledTimes(1);
+ });
+
+ it('omits Skip when allowSkip is false and rolls back on Back', async () => {
+ const setup = input({
+ allowSkip: false,
+ selectValues: ['back'],
+ validate: vi.fn(async () => ({ status: 'failed' as const })),
+ });
+
+ await expect(setup.run()).resolves.toBe('back');
+
+ expect(setup.select).toHaveBeenCalledWith({
+ message: 'Connection setup failed for warehouse',
+ options: [
+ { value: 'retry', label: 'Retry connection test' },
+ { value: 're-enter', label: 'Re-enter connection details' },
+ { value: 'back', label: 'Back' },
+ ],
+ });
+ expect(setup.rollback).toHaveBeenCalledTimes(1);
+ });
+
+ it('runs an extra action and then revalidates', async () => {
+ const action = vi.fn(async () => {});
+ let calls = 0;
+ const setup = input({
+ selectValues: ['disable-query-history'],
+ validate: vi.fn(async () => {
+ calls += 1;
+ return calls === 1
+ ? {
+ status: 'failed' as const,
+ extraActions: [
+ { value: 'disable-query-history', label: 'Disable query history and retry', run: action },
+ ],
+ }
+ : { status: 'ok' as const };
+ }),
+ });
+
+ await expect(setup.run()).resolves.toBe('ready');
+
+ expect(action).toHaveBeenCalledTimes(1);
+ expect(setup.validate).toHaveBeenCalledTimes(2);
+ });
+
+ it('rolls back when re-enter returns back or cancelled', async () => {
+ const backSetup = input({
+ selectValues: ['re-enter'],
+ configure: vi.fn(async () => 'back' as const),
+ validate: vi.fn(async () => ({ status: 'failed' as const })),
+ });
+ await expect(backSetup.run()).resolves.toBe('back');
+ expect(backSetup.rollback).toHaveBeenCalledTimes(1);
+
+ const cancelledSetup = input({
+ selectValues: ['re-enter'],
+ configure: vi.fn(async () => 'cancelled' as const),
+ validate: vi.fn(async () => ({ status: 'failed' as const })),
+ });
+ await expect(cancelledSetup.run()).resolves.toBe('failed');
+ expect(cancelledSetup.rollback).toHaveBeenCalledTimes(1);
+ });
+});
diff --git a/packages/cli/test/setup-context.test.ts b/packages/cli/test/setup-context.test.ts
index 2655527b..743cfee9 100644
--- a/packages/cli/test/setup-context.test.ts
+++ b/packages/cli/test/setup-context.test.ts
@@ -264,6 +264,7 @@ describe('setup context build state', () => {
now: () => new Date('2026-05-09T10:00:00.000Z'),
runContextBuild: runContextBuildMock,
verifyContextReady,
+ testConnection: async () => 0,
},
),
).resolves.toEqual({ status: 'ready', projectDir: tempDir, runId: 'setup-context-local-abc123' });
@@ -315,6 +316,7 @@ describe('setup context build state', () => {
runIdFactory: () => 'setup-context-local-failed',
now: () => new Date('2026-05-09T10:00:00.000Z'),
runContextBuild: runContextBuildMock,
+ testConnection: async () => 0,
},
),
).resolves.toEqual({ status: 'failed', projectDir: tempDir });
@@ -347,6 +349,7 @@ describe('setup context build state', () => {
runIdFactory: () => 'setup-context-local-throw',
now: () => new Date('2026-05-09T10:00:00.000Z'),
runContextBuild: runContextBuildMock,
+ testConnection: async () => 0,
},
),
).resolves.toEqual({
@@ -423,6 +426,7 @@ describe('setup context build state', () => {
runIdFactory: () => 'setup-context-local-enriched-scan',
now: () => new Date('2026-05-09T10:00:00.000Z'),
runContextBuild: runContextBuildMock,
+ testConnection: async () => 0,
},
),
).resolves.toEqual({ status: 'ready', projectDir: tempDir, runId: 'setup-context-local-enriched-scan' });
@@ -457,7 +461,7 @@ describe('setup context build state', () => {
runKtxSetupContextStep(
{ projectDir: tempDir, inputMode: 'disabled' },
io.io,
- { runContextBuild: runContextBuildMock },
+ { runContextBuild: runContextBuildMock, testConnection: async () => 0 },
),
).resolves.toMatchObject({ status: 'ready' });
@@ -552,10 +556,119 @@ describe('setup context build state', () => {
runKtxSetupContextStep(
{ projectDir: tempDir, inputMode: 'disabled' },
io.io,
- { runContextBuild: runContextBuildMock, verifyContextReady },
+ { runContextBuild: runContextBuildMock, verifyContextReady, testConnection: async () => 0 },
),
).resolves.toMatchObject({ status: 'ready' });
expect(runContextBuildMock).toHaveBeenCalledOnce();
});
+
+ it('blocks the build and names the failing connection without leaking raw error text', async () => {
+ const missingDbPath = join(tempDir, 'missing-warehouse.sqlite');
+ await writeReadyProject(tempDir, {
+ connections: { warehouse: { driver: 'sqlite', path: missingDbPath } },
+ });
+ const io = makeIo();
+ const runContextBuildMock = vi.fn(async () => ({ exitCode: 0 }));
+
+ await expect(
+ runKtxSetupContextStep(
+ { projectDir: tempDir, inputMode: 'disabled' },
+ io.io,
+ {
+ runIdFactory: () => 'setup-context-local-gate',
+ now: () => new Date('2026-05-09T10:00:00.000Z'),
+ runContextBuild: runContextBuildMock,
+ },
+ ),
+ ).resolves.toEqual({ status: 'failed', projectDir: tempDir });
+
+ expect(runContextBuildMock).not.toHaveBeenCalled();
+ // Names the failing connection by id + connector type, with remediation.
+ expect(io.stderr()).toContain('warehouse (sqlite)');
+ expect(io.stderr()).toContain('ktx connection test');
+ // The remediation command targets the project that just failed, not cwd.
+ expect(io.stderr()).toContain(`ktx connection test --project-dir ${tempDir}`);
+ // Never surfaces raw connection error text (or the database path) to the user.
+ expect(io.stderr()).not.toContain('File not found');
+ expect(io.stderr()).not.toContain(missingDbPath);
+ // The failed context state forces context.ready=false so setup cannot read as ready.
+ await expect(readKtxSetupContextState(tempDir)).resolves.toMatchObject({
+ status: 'failed',
+ failureReason: 'Required connections failed their live test: warehouse (sqlite).',
+ });
+ expect((await readKtxSetupState(tempDir)).completed_steps).not.toContain('context');
+ });
+
+ it('retries connection tests after a fix and then builds in interactive mode', async () => {
+ await writeReadyProject(tempDir, {
+ connections: { warehouse: { driver: 'postgres', readonly: true } },
+ });
+ const io = makeIo();
+ const runContextBuildMock = vi.fn(async () => ({ exitCode: 0 }));
+ const verifyContextReady = vi.fn(async () => ({
+ ready: true,
+ agentContextReady: true,
+ semanticSearchReady: true,
+ details: ['ready'],
+ }));
+ let gateRounds = 0;
+ const testConnection = vi.fn(async () => (++gateRounds === 1 ? 1 : 0));
+ let selectCalls = 0;
+ const select = vi.fn(async () => {
+ selectCalls += 1;
+ return selectCalls === 1 ? 'build' : 'retry';
+ });
+
+ await expect(
+ runKtxSetupContextStep(
+ { projectDir: tempDir, inputMode: 'auto' },
+ io.io,
+ {
+ prompts: { select, cancel: vi.fn() },
+ runContextBuild: runContextBuildMock,
+ verifyContextReady,
+ testConnection,
+ },
+ ),
+ ).resolves.toMatchObject({ status: 'ready' });
+
+ expect(testConnection).toHaveBeenCalledTimes(2);
+ expect(runContextBuildMock).toHaveBeenCalledOnce();
+ expect(io.stderr()).toContain('warehouse (postgres)');
+ });
+
+ it('returns to setup when the user backs out of a failing connection in interactive mode', async () => {
+ await writeReadyProject(tempDir, {
+ connections: { warehouse: { driver: 'postgres', readonly: true } },
+ });
+ const io = makeIo();
+ const runContextBuildMock = vi.fn(async () => ({ exitCode: 0 }));
+ const verifyContextReady = vi.fn(async () => ({
+ ready: true,
+ agentContextReady: true,
+ semanticSearchReady: true,
+ details: ['ready'],
+ }));
+ let selectCalls = 0;
+ const select = vi.fn(async () => {
+ selectCalls += 1;
+ return selectCalls === 1 ? 'build' : 'back';
+ });
+
+ await expect(
+ runKtxSetupContextStep(
+ { projectDir: tempDir, inputMode: 'auto' },
+ io.io,
+ {
+ prompts: { select, cancel: vi.fn() },
+ runContextBuild: runContextBuildMock,
+ verifyContextReady,
+ testConnection: async () => 1,
+ },
+ ),
+ ).resolves.toEqual({ status: 'back', projectDir: tempDir });
+
+ expect(runContextBuildMock).not.toHaveBeenCalled();
+ });
});
diff --git a/packages/cli/test/setup-databases.test.ts b/packages/cli/test/setup-databases.test.ts
index cf7acf3c..265459e2 100644
--- a/packages/cli/test/setup-databases.test.ts
+++ b/packages/cli/test/setup-databases.test.ts
@@ -1261,11 +1261,16 @@ describe('setup databases step', () => {
const prompts = makePromptAdapter({
textValues: ['env:DATABASE_URL'],
});
+ let primaryMenuCount = 0;
vi.mocked(prompts.select).mockImplementation(async (options) => {
- if (options.message === 'Databases configured: warehouse\nWhat would you like to do?') return 'edit';
+ if (options.message === 'Databases configured: warehouse\nWhat would you like to do?') {
+ primaryMenuCount += 1;
+ return primaryMenuCount === 1 ? 'edit' : 'continue';
+ }
if (options.message === 'Database to edit') return 'warehouse';
if (options.message === 'How do you want to connect to PostgreSQL?') return 'url';
if (options.message.startsWith('Enable query-history ingest')) return 'no';
+ if (options.message === 'Connection setup failed for warehouse') return 'back';
return 'back';
});
const listTables = vi.fn(async () => [
@@ -1286,13 +1291,283 @@ describe('setup databases step', () => {
},
);
- expect(result).toEqual({ status: 'failed', projectDir: tempDir });
+ expect(result).toEqual({ status: 'ready', projectDir: tempDir, connectionIds: ['warehouse'] });
const config = parseKtxProjectConfig(await readFile(join(tempDir, 'ktx.yaml'), 'utf-8'));
expect(config.connections.warehouse).toMatchObject({
enabled_tables: ['public.orders'],
});
});
+ it('recovers from an interactive database edit failure by re-entering details', async () => {
+ await writeFile(
+ join(tempDir, 'ktx.yaml'),
+ [
+ 'connections:',
+ ' analytics:',
+ ' driver: postgres',
+ ' url: env:OLD_DATABASE_URL',
+ 'setup:',
+ ' database_connection_ids:',
+ ' - analytics',
+ '',
+ ].join('\n'),
+ 'utf-8',
+ );
+ const io = makeIo();
+ const prompts = makePromptAdapter({
+ selectValues: ['edit', 'analytics', 'url', 'no', 're-enter', 'url', 'no', 'continue'],
+ textValues: ['env:BAD_DATABASE_URL', 'env:FIXED_DATABASE_URL'],
+ });
+ let attempts = 0;
+
+ const result = await runKtxSetupDatabasesStep(
+ {
+ projectDir: tempDir,
+ inputMode: 'auto',
+ databaseSchemas: [],
+ skipDatabases: false,
+ },
+ io.io,
+ {
+ prompts,
+ testConnection: vi.fn(async () => {
+ attempts += 1;
+ return attempts === 1 ? 1 : 0;
+ }),
+ scanConnection: vi.fn(async () => 0),
+ listSchemas: vi.fn(async () => ['public']),
+ listTables: vi.fn(async () => [{ catalog: null, schema: 'public', name: 'orders', kind: 'table' as const }]),
+ },
+ );
+
+ expect(result.status).toBe('ready');
+ expect(vi.mocked(prompts.select)).toHaveBeenCalledWith(
+ expect.objectContaining({
+ message: 'Connection setup failed for analytics',
+ options: expect.arrayContaining([
+ { value: 'retry', label: 'Retry connection test' },
+ { value: 're-enter', label: 'Re-enter connection details' },
+ { value: 'back', label: 'Back' },
+ ]),
+ }),
+ );
+ const config = parseKtxProjectConfig(await readFile(join(tempDir, 'ktx.yaml'), 'utf-8'));
+ expect(config.connections.analytics).toMatchObject({
+ driver: 'postgres',
+ url: 'env:FIXED_DATABASE_URL',
+ });
+ });
+
+ it('re-enters details after an interactive existing database validation failure', async () => {
+ await writeFile(
+ join(tempDir, 'ktx.yaml'),
+ [
+ 'connections:',
+ ' warehouse:',
+ ' driver: postgres',
+ ' url: env:OLD_DATABASE_URL',
+ '',
+ ].join('\n'),
+ 'utf-8',
+ );
+ const io = makeIo();
+ const prompts = makePromptAdapter({
+ selectValues: ['existing:warehouse', 'no', 're-enter', 'url', 'no'],
+ textValues: ['env:FIXED_DATABASE_URL'],
+ });
+ let attempts = 0;
+
+ const result = await runKtxSetupDatabasesStep(
+ {
+ projectDir: tempDir,
+ inputMode: 'auto',
+ databaseDrivers: ['postgres'],
+ databaseSchemas: [],
+ skipDatabases: false,
+ },
+ io.io,
+ {
+ prompts,
+ testConnection: vi.fn(async () => {
+ attempts += 1;
+ return attempts === 1 ? 1 : 0;
+ }),
+ scanConnection: vi.fn(async () => 0),
+ listSchemas: vi.fn(async () => ['public']),
+ listTables: vi.fn(async () => [
+ { catalog: null, schema: 'public', name: 'orders', kind: 'table' as const },
+ ]),
+ },
+ );
+
+ expect(result.status).toBe('ready');
+ expect(vi.mocked(prompts.select)).toHaveBeenCalledWith({
+ message: 'How do you want to connect to PostgreSQL?',
+ options: [
+ { value: 'url', label: 'Paste a connection URL' },
+ { value: 'fields', label: 'Enter connection details (host, port, database, user)' },
+ { value: 'back', label: 'Back' },
+ ],
+ });
+ expect(vi.mocked(prompts.select)).toHaveBeenCalledWith(
+ expect.objectContaining({
+ message: 'Connection setup failed for warehouse',
+ options: expect.arrayContaining([
+ { value: 'retry', label: 'Retry connection test' },
+ { value: 're-enter', label: 'Re-enter connection details' },
+ { value: 'skip', label: 'Skip this connection' },
+ { value: 'back', label: 'Back' },
+ ]),
+ }),
+ );
+ const config = parseKtxProjectConfig(await readFile(join(tempDir, 'ktx.yaml'), 'utf-8'));
+ expect(config.connections.warehouse).toMatchObject({
+ driver: 'postgres',
+ url: 'env:FIXED_DATABASE_URL',
+ });
+ });
+
+ it('restores the previous database config when backing out of a failed edit', async () => {
+ await writeFile(
+ join(tempDir, 'ktx.yaml'),
+ [
+ 'connections:',
+ ' analytics:',
+ ' driver: postgres',
+ ' url: env:OLD_DATABASE_URL',
+ 'setup:',
+ ' database_connection_ids:',
+ ' - analytics',
+ '',
+ ].join('\n'),
+ 'utf-8',
+ );
+ const io = makeIo();
+ const prompts = makePromptAdapter({
+ selectValues: ['edit', 'analytics', 'url', 'no', 'back', 'continue'],
+ textValues: ['env:BAD_DATABASE_URL'],
+ });
+
+ const result = await runKtxSetupDatabasesStep(
+ {
+ projectDir: tempDir,
+ inputMode: 'auto',
+ databaseSchemas: [],
+ skipDatabases: false,
+ },
+ io.io,
+ {
+ prompts,
+ testConnection: vi.fn(async () => 1),
+ scanConnection: vi.fn(async () => 0),
+ },
+ );
+
+ expect(result.status).toBe('ready');
+ const config = parseKtxProjectConfig(await readFile(join(tempDir, 'ktx.yaml'), 'utf-8'));
+ expect(config.connections.analytics).toMatchObject({
+ driver: 'postgres',
+ url: 'env:OLD_DATABASE_URL',
+ });
+ });
+
+ it('keeps scripted database setup fail-fast without rolling back attempted config', async () => {
+ await writeFile(
+ join(tempDir, 'ktx.yaml'),
+ [
+ 'connections:',
+ ' analytics:',
+ ' driver: postgres',
+ ' url: env:OLD_DATABASE_URL',
+ '',
+ ].join('\n'),
+ 'utf-8',
+ );
+ const io = makeIo();
+
+ const result = await runKtxSetupDatabasesStep(
+ {
+ projectDir: tempDir,
+ inputMode: 'disabled',
+ databaseConnectionIds: ['analytics'],
+ databaseSchemas: [],
+ enableQueryHistory: true,
+ skipDatabases: false,
+ },
+ io.io,
+ {
+ testConnection: vi.fn(async () => 1),
+ scanConnection: vi.fn(async () => 0),
+ },
+ );
+
+ expect(result.status).toBe('failed');
+ const config = parseKtxProjectConfig(await readFile(join(tempDir, 'ktx.yaml'), 'utf-8'));
+ expect(config.connections.analytics).toMatchObject({
+ driver: 'postgres',
+ url: 'env:OLD_DATABASE_URL',
+ context: {
+ queryHistory: {
+ enabled: true,
+ },
+ },
+ });
+ });
+
+ it('keeps scripted database ids fail-fast even when input mode is auto', async () => {
+ await writeFile(
+ join(tempDir, 'ktx.yaml'),
+ [
+ 'connections:',
+ ' analytics:',
+ ' driver: postgres',
+ ' url: env:OLD_DATABASE_URL',
+ '',
+ ].join('\n'),
+ 'utf-8',
+ );
+ const io = makeIo();
+ const prompts = makePromptAdapter({});
+ vi.mocked(prompts.select).mockImplementation(async ({ message }) => {
+ if (message === 'Connection setup failed for analytics') {
+ throw new Error('scripted selected-id setup opened the recovery menu');
+ }
+ return 'finish';
+ });
+
+ const result = await runKtxSetupDatabasesStep(
+ {
+ projectDir: tempDir,
+ inputMode: 'auto',
+ databaseConnectionIds: ['analytics'],
+ databaseSchemas: [],
+ enableQueryHistory: true,
+ skipDatabases: false,
+ },
+ io.io,
+ {
+ prompts,
+ testConnection: vi.fn(async () => 1),
+ scanConnection: vi.fn(async () => 0),
+ },
+ );
+
+ expect(result.status).toBe('failed');
+ expect(prompts.select).not.toHaveBeenCalledWith(
+ expect.objectContaining({ message: 'Connection setup failed for analytics' }),
+ );
+ const config = parseKtxProjectConfig(await readFile(join(tempDir, 'ktx.yaml'), 'utf-8'));
+ expect(config.connections.analytics).toMatchObject({
+ driver: 'postgres',
+ url: 'env:OLD_DATABASE_URL',
+ context: {
+ queryHistory: {
+ enabled: true,
+ },
+ },
+ });
+ });
+
it('lets Escape from connection fields return to connection method selection', async () => {
const prompts = makePromptAdapter({
selectValues: ['fields', 'url'],
@@ -2517,7 +2792,7 @@ describe('setup databases step', () => {
vi.mocked(prompts.select).mockImplementation(async ({ message, options }) => {
if (message.startsWith('Enable query-history ingest')) return 'yes';
if (message.includes('How much database context should KTX build?')) return 'fast';
- if (message.startsWith('Database setup failed for analytics')) {
+ if (message.startsWith('Connection setup failed for analytics')) {
failurePromptCount += 1;
failurePromptOptions.push(options);
if (failurePromptCount === 1) return 'disable-query-history';
@@ -2874,6 +3149,25 @@ describe('setup databases step', () => {
expect(io.stderr()).toContain('Missing database connection id');
});
+ it('returns missing input when a non-interactive new connection is missing required details', async () => {
+ const io = makeIo();
+
+ const result = await runKtxSetupDatabasesStep(
+ {
+ projectDir: tempDir,
+ inputMode: 'disabled',
+ databaseDrivers: ['postgres'],
+ databaseConnectionId: 'warehouse',
+ databaseSchemas: [],
+ skipDatabases: false,
+ },
+ io.io,
+ );
+
+ expect(result.status).toBe('missing-input');
+ expect(io.stderr()).toContain('Missing connection details');
+ });
+
it('accepts former ingest subcommand names as non-interactive database connection ids', async () => {
const io = makeIo();
diff --git a/packages/cli/test/setup-sources.test.ts b/packages/cli/test/setup-sources.test.ts
index 784dcc46..e4f7af2d 100644
--- a/packages/cli/test/setup-sources.test.ts
+++ b/packages/cli/test/setup-sources.test.ts
@@ -706,7 +706,18 @@ describe('setup sources step', () => {
);
expect(io.stderr()).toContain('1: Metabase database does not match KTX connection database');
expect(io.stderr()).not.toContain('Metabase mapping validation failed');
- expect(testPrompts.log).toHaveBeenCalledWith('Edit the connection or pick a different source to continue.');
+ expect(testPrompts.log).toHaveBeenCalledWith('Validating Metabase mapping...');
+ expect(testPrompts.select).toHaveBeenCalledWith(
+ expect.objectContaining({
+ message: 'Connection setup failed for metabase-main',
+ options: expect.arrayContaining([
+ { value: 'retry', label: 'Retry connection test' },
+ { value: 're-enter', label: 'Re-enter connection details' },
+ { value: 'skip', label: 'Skip this connection' },
+ { value: 'back', label: 'Back' },
+ ]),
+ }),
+ );
});
it('does not mark sources complete when validation fails', async () => {
@@ -961,7 +972,153 @@ describe('setup sources step', () => {
expect(result.status).not.toBe('failed');
expect(io.stderr()).toContain('Failed to clone https://github.com/acme/private-repo: Authentication failed');
- expect(testPrompts.log).toHaveBeenCalledWith('Edit the connection or pick a different source to continue.');
+ expect(testPrompts.select).toHaveBeenCalledWith(
+ expect.objectContaining({
+ message: 'Connection setup failed for dbt-main',
+ options: expect.arrayContaining([
+ { value: 'retry', label: 'Retry connection test' },
+ { value: 're-enter', label: 'Re-enter connection details' },
+ { value: 'skip', label: 'Skip this connection' },
+ { value: 'back', label: 'Back' },
+ ]),
+ }),
+ );
+ });
+
+ it('recovers from an existing context-source validation failure by re-entering details', async () => {
+ await addPrimarySource();
+ await addConnection('dbt-main', {
+ driver: 'dbt',
+ source_dir: '/repo/bad-dbt',
+ project_name: 'analytics',
+ });
+ let attempts = 0;
+ const validateDbt = vi.fn(async () => {
+ attempts += 1;
+ return attempts === 1
+ ? { ok: false as const, message: 'dbt project not found' }
+ : { ok: true as const, detail: 'project=analytics' };
+ });
+ const testPrompts = prompts({
+ multiselect: [['dbt']],
+ select: ['existing:dbt-main', 're-enter', 'path', 'done'],
+ text: ['/repo/fixed-dbt', ''],
+ });
+ const io = makeIo();
+
+ const result = await runKtxSetupSourcesStep(
+ { projectDir, inputMode: 'auto', runInitialSourceIngest: false, skipSources: false },
+ io.io,
+ { prompts: testPrompts, validateDbt },
+ );
+
+ expect(result.status).toBe('ready');
+ expect(validateDbt).toHaveBeenCalledTimes(2);
+ expect(vi.mocked(testPrompts.select)).toHaveBeenCalledWith(
+ expect.objectContaining({
+ message: 'Connection setup failed for dbt-main',
+ options: expect.arrayContaining([
+ { value: 'retry', label: 'Retry connection test' },
+ { value: 're-enter', label: 'Re-enter connection details' },
+ { value: 'skip', label: 'Skip this connection' },
+ { value: 'back', label: 'Back' },
+ ]),
+ }),
+ );
+ expect((await readConfig()).connections['dbt-main']).toMatchObject({
+ driver: 'dbt',
+ source_dir: '/repo/fixed-dbt',
+ });
+ });
+
+ it('restores a context-source edit and adapter enablement when recovery goes back', async () => {
+ await addPrimarySource();
+ await addConnection('dbt-main', {
+ driver: 'dbt',
+ source_dir: '/repo/existing-dbt',
+ project_name: 'analytics',
+ });
+ const testPrompts = prompts({
+ multiselect: [['dbt']],
+ select: ['edit:dbt-main', 'path', 'back'],
+ text: ['/repo/bad-dbt', ''],
+ });
+ const io = makeIo();
+
+ const result = await runKtxSetupSourcesStep(
+ { projectDir, inputMode: 'auto', runInitialSourceIngest: false, skipSources: false },
+ io.io,
+ {
+ prompts: testPrompts,
+ validateDbt: vi.fn(async () => ({ ok: false as const, message: 'dbt project not found' })),
+ },
+ );
+
+ expect(result.status).toBe('skipped');
+ const config = await readConfig();
+ expect(config.connections['dbt-main']).toMatchObject({
+ driver: 'dbt',
+ source_dir: '/repo/existing-dbt',
+ project_name: 'analytics',
+ });
+ expect(config.ingest.adapters).not.toContain('dbt');
+ });
+
+ it('lets Metabase mapping failure retry through source recovery', async () => {
+ await addPrimarySource();
+ let mappingAttempts = 0;
+ const runMapping = vi.fn(async () => {
+ mappingAttempts += 1;
+ return mappingAttempts === 1 ? 1 : 0;
+ });
+ const testPrompts = prompts({
+ multiselect: [['metabase']],
+ select: ['env', 'retry', 'done'],
+ text: ['metabase-main', 'https://metabase.example.com'],
+ });
+ const io = makeIo();
+
+ const result = await runKtxSetupSourcesStep(
+ { projectDir, inputMode: 'auto', runInitialSourceIngest: false, skipSources: false },
+ io.io,
+ {
+ prompts: testPrompts,
+ discoverMetabaseDatabases: vi.fn(async () => [
+ { id: 1, name: 'Analytics', engine: 'postgres', host: 'db.example.com', dbName: 'analytics' },
+ ]),
+ runMapping,
+ },
+ );
+
+ expect(result.status).toBe('ready');
+ expect(runMapping).toHaveBeenCalledTimes(2);
+ });
+
+ it('keeps noninteractive source setup fail-fast without rolling back attempted config', async () => {
+ await addPrimarySource();
+ const io = makeIo();
+
+ const result = await runKtxSetupSourcesStep(
+ {
+ projectDir,
+ inputMode: 'disabled',
+ source: 'lookml',
+ sourceConnectionId: 'looker-repo',
+ sourceGitUrl: 'https://github.com/acme/lookml.git',
+ runInitialSourceIngest: false,
+ skipSources: false,
+ },
+ io.io,
+ {
+ validateLookml: vi.fn(async () => ({ ok: false as const, message: 'No LookML files found' })),
+ },
+ );
+
+ expect(result.status).toBe('failed');
+ expect((await readConfig()).connections['looker-repo']).toMatchObject({
+ driver: 'lookml',
+ repoUrl: 'https://github.com/acme/lookml.git',
+ });
});
it('adds a dbt source connection and enables its adapter', async () => {
@@ -1371,7 +1528,17 @@ describe('setup sources step', () => {
source_dir: '/repo/new-dbt',
}));
expect(io.stderr()).toContain('dbt project not found');
- expect(testPrompts.log).toHaveBeenCalledWith('Edit the connection or pick a different source to continue.');
+ expect(testPrompts.select).toHaveBeenCalledWith(
+ expect.objectContaining({
+ message: 'Connection setup failed for dbt-main',
+ options: expect.arrayContaining([
+ { value: 'retry', label: 'Retry connection test' },
+ { value: 're-enter', label: 'Re-enter connection details' },
+ { value: 'skip', label: 'Skip this connection' },
+ { value: 'back', label: 'Back' },
+ ]),
+ }),
+ );
const config = await readConfig();
expect(config.connections['dbt-main']).toMatchObject({
driver: 'dbt',
From e70ae1e63bcd7168ade90b8998a06b561ce36cf2 Mon Sep 17 00:00:00 2001
From: Andrey Avtomonov
Date: Wed, 3 Jun 2026 17:19:42 +0200
Subject: [PATCH 13/49] feat(query-history): scope mining to modeled schemas by
default (#258)
* feat(query-history): structure SQL analysis table refs
* feat(query-history): qualify SQL analysis table refs
* feat(query-history): wire modeled scope floor through ingest
* chore(query-history): verify scope floor
* test(query-history): align daemon SQL batch endpoint contract
* feat(query-history): build scope from same-run scan catalog
* feat(query-history): fail open on scope-floor catalog failures
* chore(query-history): verify scope-floor v1 closure
* refactor(query-history): share scope membership
* feat(setup): apply derived query history filters
* docs: document derived query history filters
* fix(query-history): redact filter picker LLM prompt SQL
* fix(setup): run filter picker SQL analysis through managed daemon
* chore(query-history): verify filter picker v1 closure
* fix(query-history): fail open on partial service-account attribution
* fix(query-history): aggregate BigQuery users by execution count
* fix(query-history): aggregate Snowflake users by execution count
* fix(query-history): use BigQuery query info hash
---
.../content/docs/cli-reference/ktx-setup.mdx | 7 +
.../content/docs/configuration/ktx-yaml.mdx | 13 +
.../content/docs/guides/building-context.mdx | 5 +-
.../bigquery-query-history-reader.ts | 91 +++-
.../adapters/historic-sql/chunk-unified.ts | 3 +-
.../adapters/historic-sql/pattern-inputs.ts | 10 +-
.../query-history-filter-picker.ts | 278 +++++++++++++
.../adapters/historic-sql/scope-floor.ts | 260 ++++++++++++
.../adapters/historic-sql/scope-membership.ts | 45 ++
.../snowflake-query-history-reader.ts | 87 +++-
.../adapters/historic-sql/stage-unified.ts | 168 +++-----
.../ingest/adapters/historic-sql/types.ts | 18 +-
.../cli/src/context/ingest/local-adapters.ts | 36 +-
.../sql-analysis/http-sql-analysis-port.ts | 39 +-
.../cli/src/context/sql-analysis/ports.ts | 17 +-
packages/cli/src/local-adapters.ts | 36 +-
packages/cli/src/public-ingest.ts | 89 +++-
packages/cli/src/setup-databases.ts | 229 ++++++++++-
packages/cli/src/setup.ts | 3 +
.../bigquery-query-history-reader.test.ts | 27 +-
.../historic-sql/chunk-unified.test.ts | 16 +-
.../historic-sql/historic-sql.adapter.test.ts | 5 +-
.../local-ingest-acceptance.test.ts | 5 +-
.../historic-sql/pattern-inputs.test.ts | 13 +-
.../historic-sql/postgres-pgss-reader.test.ts | 2 +-
.../query-history-filter-picker.test.ts | 274 ++++++++++++
.../adapters/historic-sql/scope-floor.test.ts | 194 +++++++++
.../historic-sql/scope-membership.test.ts | 51 +++
.../snowflake-query-history-reader.test.ts | 22 +-
.../historic-sql/stage-unified.test.ts | 389 ++++++++++++++++--
.../adapters/historic-sql/types.test.ts | 3 +-
.../context/ingest/local-adapters.test.ts | 100 ++++-
.../http-sql-analysis-port.test.ts | 68 ++-
packages/cli/test/local-adapters.test.ts | 114 ++++-
packages/cli/test/managed-python-http.test.ts | 4 +-
packages/cli/test/public-ingest.test.ts | 126 +++++-
packages/cli/test/setup-databases.test.ts | 256 ++++++++++++
packages/cli/test/setup.test.ts | 3 +
packages/cli/test/sql.test.ts | 2 +-
.../ktx-daemon/src/ktx_daemon/sql_analysis.py | 139 +++++--
python/ktx-daemon/tests/test_app.py | 4 +-
python/ktx-daemon/tests/test_sql_analysis.py | 113 ++++-
42 files changed, 3090 insertions(+), 274 deletions(-)
create mode 100644 packages/cli/src/context/ingest/adapters/historic-sql/query-history-filter-picker.ts
create mode 100644 packages/cli/src/context/ingest/adapters/historic-sql/scope-floor.ts
create mode 100644 packages/cli/src/context/ingest/adapters/historic-sql/scope-membership.ts
create mode 100644 packages/cli/test/context/ingest/adapters/historic-sql/query-history-filter-picker.test.ts
create mode 100644 packages/cli/test/context/ingest/adapters/historic-sql/scope-floor.test.ts
create mode 100644 packages/cli/test/context/ingest/adapters/historic-sql/scope-membership.test.ts
diff --git a/docs-site/content/docs/cli-reference/ktx-setup.mdx b/docs-site/content/docs/cli-reference/ktx-setup.mdx
index 24469a63..0e6cb57c 100644
--- a/docs-site/content/docs/cli-reference/ktx-setup.mdx
+++ b/docs-site/content/docs/cli-reference/ktx-setup.mdx
@@ -148,6 +148,13 @@ fix the prerequisite. If the later schema-context build also fails, interactive
setup offers **Disable query history and retry** so you can finish database
setup with `connections..context.queryHistory.enabled: false`.
+After the schema scan completes, setup can derive query-history service-account
+filters from in-scope history. If **ktx** finds clear operational roles, it
+prints each proposed exclusion with a reason and writes
+`connections..context.queryHistory.filters.serviceAccounts` only when you
+apply the proposal. In non-interactive setup with `--yes`, the proposal is
+applied automatically. Existing `serviceAccounts` blocks are never overwritten.
+
For BigQuery, the remediation tells you to grant `roles/bigquery.resourceViewer`
on the BigQuery project, or grant a custom role that contains
`bigquery.jobs.listAll`.
diff --git a/docs-site/content/docs/configuration/ktx-yaml.mdx b/docs-site/content/docs/configuration/ktx-yaml.mdx
index a9298443..17a04c53 100644
--- a/docs-site/content/docs/configuration/ktx-yaml.mdx
+++ b/docs-site/content/docs/configuration/ktx-yaml.mdx
@@ -179,9 +179,22 @@ connections:
context:
queryHistory:
enabled: true
+ enabledSchemas:
+ - orbit_raw
+ - orbit_analytics
minExecutions: 5
```
+- `enabledSchemas`: Optional list of schema or dataset names that query-history
+ ingest may mine. Omit it to let **ktx** derive the modeled schema floor from
+ the connection and semantic-layer sources. Use `["*"]` to disable the floor
+ for discovery runs.
+- `filters.serviceAccounts`: Optional service-account filter block. During
+ setup, when query history is enabled and no service-account block already
+ exists, **ktx** can propose exact role patterns such as `^svc_loader$` from
+ observed in-scope query history. The block uses `mode: exclude` and remains
+ hand-editable.
+
### Metabase
```yaml
diff --git a/docs-site/content/docs/guides/building-context.mdx b/docs-site/content/docs/guides/building-context.mdx
index 52179e70..9bcf2659 100644
--- a/docs-site/content/docs/guides/building-context.mdx
+++ b/docs-site/content/docs/guides/building-context.mdx
@@ -57,7 +57,10 @@ isolation.
## Query history
PostgreSQL, BigQuery, and Snowflake can add query-history context: common joins,
-filters, service-account patterns, redaction rules, and high-usage templates.
+filters, redaction rules, high-usage templates, and service-account exclusions.
+When query history is enabled during setup, **ktx** reviews observed in-scope
+roles and can write exact `filters.serviceAccounts` patterns for operational
+traffic such as loader or refresh roles.
Enable it during setup, store it under `connections..context.queryHistory`,
or request it for one run:
diff --git a/packages/cli/src/context/ingest/adapters/historic-sql/bigquery-query-history-reader.ts b/packages/cli/src/context/ingest/adapters/historic-sql/bigquery-query-history-reader.ts
index ac4d4c71..fe637078 100644
--- a/packages/cli/src/context/ingest/adapters/historic-sql/bigquery-query-history-reader.ts
+++ b/packages/cli/src/context/ingest/adapters/historic-sql/bigquery-query-history-reader.ts
@@ -200,27 +200,78 @@ export class BigQueryHistoricSqlQueryHistoryReader {
config: HistoricSqlUnifiedPullConfig,
): AsyncIterable {
const sql = `
+WITH filtered_jobs AS (
+ SELECT
+ COALESCE(query_info.query_hashes.normalized_literals, TO_HEX(SHA256(query))) AS template_id,
+ query,
+ user_email,
+ creation_time,
+ end_time,
+ error_result
+ FROM ${this.viewPath}
+ WHERE job_type = 'QUERY'
+ AND statement_type IN ('SELECT', 'MERGE')
+ AND creation_time >= ${timestampExpression(window.start)}
+ AND creation_time < ${timestampExpression(window.end)}
+ AND query IS NOT NULL
+),
+template_stats AS (
+ SELECT
+ template_id,
+ MIN(query) AS canonical_sql,
+ COUNT(*) AS executions,
+ COUNT(DISTINCT user_email) AS distinct_users,
+ MIN(creation_time) AS first_seen,
+ MAX(creation_time) AS last_seen,
+ APPROX_QUANTILES(TIMESTAMP_DIFF(end_time, creation_time, MILLISECOND), 100)[OFFSET(50)] AS p50_ms,
+ APPROX_QUANTILES(TIMESTAMP_DIFF(end_time, creation_time, MILLISECOND), 100)[OFFSET(95)] AS p95_ms,
+ SAFE_DIVIDE(COUNTIF(error_result IS NOT NULL), COUNT(*)) AS error_rate,
+ CAST(NULL AS INT64) AS rows_produced
+ FROM filtered_jobs
+ GROUP BY template_id
+ HAVING COUNT(*) >= ${config.minExecutions}
+),
+template_users AS (
+ SELECT
+ template_id,
+ user_email AS user,
+ COUNT(*) AS executions,
+ MAX(creation_time) AS last_seen
+ FROM filtered_jobs
+ GROUP BY template_id, user_email
+)
SELECT
- query_hash AS template_id,
- MIN(query) AS canonical_sql,
- COUNT(*) AS executions,
- COUNT(DISTINCT user_email) AS distinct_users,
- MIN(creation_time) AS first_seen,
- MAX(creation_time) AS last_seen,
- APPROX_QUANTILES(TIMESTAMP_DIFF(end_time, creation_time, MILLISECOND), 100)[OFFSET(50)] AS p50_ms,
- APPROX_QUANTILES(TIMESTAMP_DIFF(end_time, creation_time, MILLISECOND), 100)[OFFSET(95)] AS p95_ms,
- SAFE_DIVIDE(COUNTIF(error_result IS NOT NULL), COUNT(*)) AS error_rate,
- CAST(NULL AS INT64) AS rows_produced,
- TO_JSON_STRING(ARRAY_AGG(STRUCT(user_email AS user, 1 AS executions) ORDER BY creation_time DESC LIMIT 5)) AS top_users
-FROM ${this.viewPath}
-WHERE job_type = 'QUERY'
- AND statement_type IN ('SELECT', 'MERGE')
- AND creation_time >= ${timestampExpression(window.start)}
- AND creation_time < ${timestampExpression(window.end)}
- AND query IS NOT NULL
-GROUP BY query_hash
-HAVING COUNT(*) >= ${config.minExecutions}
-ORDER BY executions DESC`.trim();
+ stats.template_id,
+ stats.canonical_sql,
+ stats.executions,
+ stats.distinct_users,
+ stats.first_seen,
+ stats.last_seen,
+ stats.p50_ms,
+ stats.p95_ms,
+ stats.error_rate,
+ stats.rows_produced,
+ TO_JSON_STRING(
+ ARRAY_AGG(
+ STRUCT(users.user AS user, users.executions AS executions)
+ ORDER BY users.executions DESC, users.last_seen DESC
+ )
+ ) AS top_users
+FROM template_stats AS stats
+JOIN template_users AS users
+ ON users.template_id = stats.template_id
+GROUP BY
+ stats.template_id,
+ stats.canonical_sql,
+ stats.executions,
+ stats.distinct_users,
+ stats.first_seen,
+ stats.last_seen,
+ stats.p50_ms,
+ stats.p95_ms,
+ stats.error_rate,
+ stats.rows_produced
+ORDER BY stats.executions DESC`.trim();
const result = await queryClient(client).executeQuery(sql);
if (result.error) {
throw grantsError(result.error);
diff --git a/packages/cli/src/context/ingest/adapters/historic-sql/chunk-unified.ts b/packages/cli/src/context/ingest/adapters/historic-sql/chunk-unified.ts
index 4477e753..56cc32e7 100644
--- a/packages/cli/src/context/ingest/adapters/historic-sql/chunk-unified.ts
+++ b/packages/cli/src/context/ingest/adapters/historic-sql/chunk-unified.ts
@@ -1,6 +1,7 @@
import { createHash } from 'node:crypto';
import { readFile, readdir } from 'node:fs/promises';
import { join, relative } from 'node:path';
+import { tableRefKey } from '../../../scan/table-ref.js';
import type { ChunkResult, DiffSet, ScopeDescriptor, WorkUnit } from '../../types.js';
import { isHistoricSqlPatternInputShardPath } from './pattern-inputs.js';
import { stagedManifestSchema, stagedPatternsInputSchema, stagedTableInputSchema } from './types.js';
@@ -37,7 +38,7 @@ export async function chunkHistoricSqlUnifiedStagedDir(stagedDir: string, diffSe
}
const table = stagedTableInputSchema.parse(await readJson(stagedDir, path));
workUnits.push({
- unitKey: `historic-sql-table-${safeUnitKey(table.table)}`,
+ unitKey: `historic-sql-table-${safeUnitKey(tableRefKey(table.tableRef))}`,
displayLabel: `Historic SQL usage: ${table.table}`,
rawFiles: [path],
dependencyPaths: ['manifest.json'],
diff --git a/packages/cli/src/context/ingest/adapters/historic-sql/pattern-inputs.ts b/packages/cli/src/context/ingest/adapters/historic-sql/pattern-inputs.ts
index 025fa43c..2e99836e 100644
--- a/packages/cli/src/context/ingest/adapters/historic-sql/pattern-inputs.ts
+++ b/packages/cli/src/context/ingest/adapters/historic-sql/pattern-inputs.ts
@@ -1,4 +1,5 @@
import { Buffer } from 'node:buffer';
+import { tableRefKey } from '../../../scan/table-ref.js';
import type { StagedPatternsInput } from './types.js';
const HISTORIC_SQL_PATTERN_WORKUNIT_DIR = 'patterns-input';
@@ -44,11 +45,16 @@ function sortedAuditTemplates(templates: readonly PatternTemplate[]): PatternTem
function sortedPatternCandidates(templates: readonly PatternTemplate[]): PatternTemplate[] {
return [...templates]
.filter((template) => template.tablesTouched.length >= 2)
- .map((template) => ({ ...template, tablesTouched: [...template.tablesTouched].sort() }))
+ .map((template) => ({
+ ...template,
+ tablesTouched: [...template.tablesTouched].sort((left, right) => tableRefKey(left).localeCompare(tableRefKey(right))),
+ }))
.sort((left, right) => {
const cardinality = right.tablesTouched.length - left.tablesTouched.length;
if (cardinality !== 0) return cardinality;
- const tableSignature = left.tablesTouched.join('\0').localeCompare(right.tablesTouched.join('\0'));
+ const leftSignature = left.tablesTouched.map(tableRefKey).join('\0');
+ const rightSignature = right.tablesTouched.map(tableRefKey).join('\0');
+ const tableSignature = leftSignature.localeCompare(rightSignature);
if (tableSignature !== 0) return tableSignature;
return left.id.localeCompare(right.id);
});
diff --git a/packages/cli/src/context/ingest/adapters/historic-sql/query-history-filter-picker.ts b/packages/cli/src/context/ingest/adapters/historic-sql/query-history-filter-picker.ts
new file mode 100644
index 00000000..bb296513
--- /dev/null
+++ b/packages/cli/src/context/ingest/adapters/historic-sql/query-history-filter-picker.ts
@@ -0,0 +1,278 @@
+import { z } from 'zod';
+import type { KtxLlmRuntimePort } from '../../../../context/llm/runtime-port.js';
+import type { SqlAnalysisPort } from '../../../../context/sql-analysis/ports.js';
+import { tableRefKey } from '../../../scan/table-ref.js';
+import type { KtxTableRef } from '../../../scan/types.js';
+import { bucketDistinctUsers, bucketExecutions, bucketRecency } from './buckets.js';
+import {
+ compileHistoricSqlRedactionPatterns,
+ redactHistoricSqlText,
+ type HistoricSqlRedactionPattern,
+} from './redaction.js';
+import { includedQueryHistoryTableRefs } from './scope-membership.js';
+import {
+ aggregatedTemplateSchema,
+ historicSqlUnifiedPullConfigSchema,
+ type AggregatedTemplate,
+ type HistoricSqlDialect,
+ type HistoricSqlReader,
+} from './types.js';
+
+export interface QueryHistoryFilterProposal {
+ excludedRoles: Array<{ role: string; reason: string; pattern: string }>;
+ consideredRoleCount: number;
+ skipped: { reason: 'no-llm' | 'no-daemon' | 'no-in-scope-history' | 'user-block-present' } | null;
+ warnings: string[];
+}
+
+export interface ProposeQueryHistoryServiceAccountFiltersInput {
+ connectionId: string;
+ dialect: HistoricSqlDialect;
+ queryClient: unknown;
+ reader: HistoricSqlReader;
+ sqlAnalysis: SqlAnalysisPort;
+ llmRuntime: KtxLlmRuntimePort | null;
+ pullConfig: unknown;
+ now?: Date;
+ userServiceAccountsPresent?: boolean;
+}
+
+interface ParsedTemplateForPicker {
+ template: AggregatedTemplate;
+ tablesTouched: KtxTableRef[];
+ includedTables: KtxTableRef[];
+}
+
+interface RoleAccumulator {
+ role: string;
+ executions: number;
+ distinctUsers: number;
+ lastSeen: string;
+ tables: Map;
+ templates: AggregatedTemplate[];
+}
+
+interface QueryHistoryRoleRecord {
+ role: string;
+ inScopeTables: string[];
+ executionsBucket: string;
+ distinctUsersBucket: string;
+ recencyBucket: string;
+ representativeTemplates: Array<{ id: string; canonicalSql: string; dialect: HistoricSqlDialect }>;
+}
+
+const queryHistoryFilterAdjudicationSchema = z.object({
+ roles: z.array(
+ z.object({
+ role: z.string().min(1),
+ exclude: z.boolean(),
+ reason: z.string().min(1),
+ }).strict(),
+ ),
+}).strict();
+
+type QueryHistoryFilterAdjudication = z.infer;
+
+function emptyProposal(skipped: QueryHistoryFilterProposal['skipped'], warnings: string[] = []): QueryHistoryFilterProposal {
+ return { excludedRoles: [], consideredRoleCount: 0, skipped, warnings };
+}
+
+function displayTableRef(ref: KtxTableRef): string {
+ return [ref.catalog, ref.db, ref.name].filter((part): part is string => !!part && part.length > 0).join('.');
+}
+
+function redactTemplateSqlForPicker(
+ template: AggregatedTemplate,
+ redactors: readonly HistoricSqlRedactionPattern[],
+): AggregatedTemplate {
+ if (redactors.length === 0) {
+ return template;
+ }
+ return {
+ ...template,
+ canonicalSql: redactHistoricSqlText(template.canonicalSql, redactors),
+ };
+}
+
+/** @internal */
+export function regexEscapeForExactRolePattern(role: string): string {
+ return `^${role.replace(/[\\^$.*+?()[\]{}|]/g, '\\$&')}$`;
+}
+
+function recordRole(
+ acc: RoleAccumulator,
+ template: AggregatedTemplate,
+ tables: readonly KtxTableRef[],
+ executions: number,
+): void {
+ acc.executions += executions;
+ acc.distinctUsers = Math.max(acc.distinctUsers, template.stats.distinctUsers);
+ acc.lastSeen = template.stats.lastSeen > acc.lastSeen ? template.stats.lastSeen : acc.lastSeen;
+ for (const table of tables) {
+ acc.tables.set(tableRefKey(table), table);
+ }
+ acc.templates.push(template);
+}
+
+function roleRecords(parsedTemplates: readonly ParsedTemplateForPicker[], now: Date): QueryHistoryRoleRecord[] {
+ const byRole = new Map();
+ for (const parsed of parsedTemplates) {
+ for (const entry of parsed.template.topUsers) {
+ if (!entry.user || entry.user.trim().length === 0 || entry.executions <= 0) {
+ continue;
+ }
+ const role = entry.user.trim();
+ const acc =
+ byRole.get(role) ??
+ {
+ role,
+ executions: 0,
+ distinctUsers: 0,
+ lastSeen: '1970-01-01T00:00:00.000Z',
+ tables: new Map(),
+ templates: [],
+ };
+ recordRole(acc, parsed.template, parsed.includedTables, entry.executions);
+ byRole.set(role, acc);
+ }
+ }
+
+ return [...byRole.values()]
+ .sort((left, right) => right.executions - left.executions || left.role.localeCompare(right.role))
+ .map((acc) => ({
+ role: acc.role,
+ inScopeTables: [...acc.tables.entries()]
+ .sort(([left], [right]) => left.localeCompare(right))
+ .slice(0, 25)
+ .map(([, ref]) => displayTableRef(ref)),
+ executionsBucket: bucketExecutions(acc.executions),
+ distinctUsersBucket: bucketDistinctUsers(acc.distinctUsers),
+ recencyBucket: bucketRecency(acc.lastSeen, now),
+ representativeTemplates: [...acc.templates]
+ .sort((left, right) => right.stats.executions - left.stats.executions || left.templateId.localeCompare(right.templateId))
+ .slice(0, 3)
+ .map((template) => ({
+ id: template.templateId,
+ canonicalSql: template.canonicalSql,
+ dialect: template.dialect,
+ })),
+ }));
+}
+
+function adjudicationSystemPrompt(): string {
+ return [
+ 'You are helping ktx decide whether observed query-history roles are operational service accounts.',
+ 'Default every role to keep. Mark exclude true only when the aggregate evidence clearly shows loader, ELT, reverse-ETL, export, refresh, or maintenance traffic rather than analyst or BI-dashboard usage.',
+ 'Use only the observed role records. Do not rely on a hardcoded denylist. Return structured output only.',
+ ].join('\n');
+}
+
+export async function proposeQueryHistoryServiceAccountFilters(
+ input: ProposeQueryHistoryServiceAccountFiltersInput,
+): Promise {
+ if (!input.llmRuntime) {
+ return emptyProposal({ reason: 'no-llm' });
+ }
+
+ const config = historicSqlUnifiedPullConfigSchema.parse(input.pullConfig);
+ const redactors = compileHistoricSqlRedactionPatterns(config.redactionPatterns);
+ const now = input.now ?? new Date();
+ const windowDays = 'windowDays' in config ? config.windowDays : 90;
+ const windowStart = new Date(now.getTime() - windowDays * 24 * 60 * 60 * 1000);
+ const warnings: string[] = [];
+ const snapshot: AggregatedTemplate[] = [];
+
+ try {
+ for await (const row of input.reader.fetchAggregated(input.queryClient, { start: windowStart, end: now }, config)) {
+ snapshot.push(aggregatedTemplateSchema.parse(row));
+ }
+ } catch (error) {
+ return emptyProposal(null, [
+ `query_history_filter_picker_read_failed:${error instanceof Error ? error.message : String(error)}`,
+ ]);
+ }
+
+ if (snapshot.length === 0) {
+ return emptyProposal({ reason: 'no-in-scope-history' });
+ }
+
+ const analysisItems = snapshot.map((template) => ({ id: template.templateId, sql: template.canonicalSql }));
+ const analysisOptions =
+ config.modeledTableCatalog.length > 0 ? { catalog: { tables: config.modeledTableCatalog } } : undefined;
+ let analysis: Awaited>;
+ try {
+ analysis = await input.sqlAnalysis.analyzeBatch(analysisItems, input.dialect, analysisOptions);
+ } catch (error) {
+ return emptyProposal({ reason: 'no-daemon' }, [
+ `query_history_filter_picker_analysis_failed:${error instanceof Error ? error.message : String(error)}`,
+ ]);
+ }
+
+ const parsedTemplates: ParsedTemplateForPicker[] = [];
+ for (const template of snapshot) {
+ const parsed = analysis.get(template.templateId);
+ if (!parsed || parsed.error) {
+ warnings.push(`query_history_filter_picker_parse_failed:${template.templateId}`);
+ continue;
+ }
+ const tablesTouched = [...new Map(parsed.tablesTouched.map((ref) => [tableRefKey(ref), ref])).values()]
+ .filter((ref) => ref.name.length > 0)
+ .sort((left, right) => tableRefKey(left).localeCompare(tableRefKey(right)));
+ const includedTables = includedQueryHistoryTableRefs(tablesTouched, config);
+ if (includedTables.length === 0) {
+ continue;
+ }
+ parsedTemplates.push({
+ template: redactTemplateSqlForPicker(template, redactors),
+ tablesTouched,
+ includedTables,
+ });
+ }
+
+ const records = roleRecords(parsedTemplates, now);
+ if (records.length <= 1) {
+ return {
+ excludedRoles: [],
+ consideredRoleCount: records.length,
+ skipped: { reason: 'no-in-scope-history' },
+ warnings,
+ };
+ }
+
+ let generated: QueryHistoryFilterAdjudication;
+ try {
+ generated = await input.llmRuntime.generateObject({
+ role: 'candidateExtraction',
+ system: adjudicationSystemPrompt(),
+ prompt: JSON.stringify({ connectionId: input.connectionId, dialect: input.dialect, roles: records }),
+ schema: queryHistoryFilterAdjudicationSchema,
+ });
+ } catch (error) {
+ return {
+ excludedRoles: [],
+ consideredRoleCount: records.length,
+ skipped: { reason: 'no-llm' },
+ warnings: [
+ ...warnings,
+ `query_history_filter_picker_llm_failed:${error instanceof Error ? error.message : String(error)}`,
+ ],
+ };
+ }
+
+ const knownRoles = new Set(records.map((record) => record.role));
+ const excludedRoles = generated.roles
+ .filter((role) => role.exclude && knownRoles.has(role.role))
+ .sort((left, right) => left.role.localeCompare(right.role))
+ .map((role) => ({
+ role: role.role,
+ reason: role.reason,
+ pattern: regexEscapeForExactRolePattern(role.role),
+ }));
+
+ return {
+ excludedRoles,
+ consideredRoleCount: records.length,
+ skipped: input.userServiceAccountsPresent ? { reason: 'user-block-present' } : null,
+ warnings,
+ };
+}
diff --git a/packages/cli/src/context/ingest/adapters/historic-sql/scope-floor.ts b/packages/cli/src/context/ingest/adapters/historic-sql/scope-floor.ts
new file mode 100644
index 00000000..23b36a0e
--- /dev/null
+++ b/packages/cli/src/context/ingest/adapters/historic-sql/scope-floor.ts
@@ -0,0 +1,260 @@
+import type { Dirent } from 'node:fs';
+import { access, readdir, readFile } from 'node:fs/promises';
+import { join, relative } from 'node:path';
+import YAML from 'yaml';
+import { getDriverRegistration } from '../../../connections/drivers.js';
+import { parseDottedTableEntry } from '../../../scan/enabled-tables.js';
+import { tableRefKey, tableRefSet, type KtxTableRefKey } from '../../../scan/table-ref.js';
+import type { KtxTableRef } from '../../../scan/types.js';
+import { readLiveDatabaseTableFiles } from '../live-database/stage.js';
+
+export interface QueryHistoryScopeFloorInput {
+ projectDir: string;
+ connectionId: string;
+ driver: string;
+ connection: Record;
+ storedQueryHistory: Record;
+}
+
+export interface QueryHistoryScopeFloor {
+ enabledTables: KtxTableRef[];
+ enabledTableKeys: ReadonlySet | null;
+ enabledSchemas: string[];
+ modeledTableCatalog: KtxTableRef[];
+ floorDisabled: boolean;
+ warnings: string[];
+}
+
+function isRecord(value: unknown): value is Record {
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
+}
+
+function stringArray(value: unknown): string[] {
+ return Array.isArray(value)
+ ? value
+ .filter((item): item is string => typeof item === 'string' && item.trim().length > 0)
+ .map((item) => item.trim())
+ : [];
+}
+
+function tableRefsFromValues(values: unknown): KtxTableRef[] {
+ if (!Array.isArray(values)) return [];
+ return values.flatMap((value) => {
+ if (typeof value === 'string') {
+ const ref = parseDottedTableEntry(value);
+ return ref ? [ref] : [];
+ }
+ if (isRecord(value) && typeof value.name === 'string' && value.name.length > 0) {
+ return [
+ {
+ catalog: typeof value.catalog === 'string' ? value.catalog : null,
+ db: typeof value.db === 'string' ? value.db : null,
+ name: value.name,
+ },
+ ];
+ }
+ return [];
+ });
+}
+
+function declaredSchemas(driver: string, connection: Record): string[] {
+ const key = getDriverRegistration(driver)?.scopeConfigKey;
+ if (!key) return [];
+ return [...new Set(stringArray(connection[key]))].sort();
+}
+
+function uniqueSortedTableRefs(refs: readonly KtxTableRef[]): KtxTableRef[] {
+ const byKey = new Map();
+ for (const ref of refs) {
+ byKey.set(tableRefKey(ref), ref);
+ }
+ return [...byKey.entries()]
+ .sort(([left], [right]) => left.localeCompare(right))
+ .map(([, ref]) => ref);
+}
+
+async function latestLiveDatabaseScanDir(projectDir: string, connectionId: string): Promise {
+ const root = join(projectDir, 'raw-sources', connectionId, 'live-database');
+ let entries: Dirent[];
+ try {
+ entries = await readdir(root, { withFileTypes: true });
+ } catch (error) {
+ if (error && typeof error === 'object' && 'code' in error && error.code === 'ENOENT') return null;
+ throw error;
+ }
+ const syncDirs = entries
+ .filter((entry) => entry.isDirectory())
+ .map((entry) => entry.name)
+ .sort()
+ .reverse();
+ for (const syncDir of syncDirs) {
+ const absolute = join(root, syncDir);
+ try {
+ await access(join(absolute, 'connection.json'));
+ return absolute;
+ } catch {
+ continue;
+ }
+ }
+ return null;
+}
+
+async function scannedTableRefs(
+ projectDir: string,
+ connectionId: string,
+): Promise<{ refs: KtxTableRef[]; catalogAvailable: boolean; warnings: string[] }> {
+ const scanDir = await latestLiveDatabaseScanDir(projectDir, connectionId);
+ if (!scanDir) {
+ return { refs: [], catalogAvailable: false, warnings: [] };
+ }
+ try {
+ const tableFiles = await readLiveDatabaseTableFiles(scanDir);
+ return {
+ refs: uniqueSortedTableRefs(
+ tableFiles.map(({ table }) => ({ catalog: table.catalog, db: table.db, name: table.name })),
+ ),
+ catalogAvailable: true,
+ warnings: [],
+ };
+ } catch (error) {
+ return {
+ refs: [],
+ catalogAvailable: false,
+ warnings: [
+ `query_history_scope_floor_catalog_read_failed:live_database_scan:${error instanceof Error ? error.message : String(error)}`,
+ ],
+ };
+ }
+}
+
+async function listYamlFiles(root: string): Promise {
+ try {
+ const entries = await readdir(root, { withFileTypes: true, recursive: true });
+ return entries
+ .filter((entry) => entry.isFile() && /\.ya?ml$/i.test(entry.name))
+ .map((entry) => relative(root, join(entry.parentPath, entry.name)).replace(/\\/g, '/'))
+ .sort();
+ } catch (error) {
+ if (error && typeof error === 'object' && 'code' in error && error.code === 'ENOENT') return [];
+ throw error;
+ }
+}
+
+function refsFromManifest(content: string): KtxTableRef[] {
+ const parsed = YAML.parse(content) as unknown;
+ if (!isRecord(parsed) || !isRecord(parsed.tables)) return [];
+ return Object.values(parsed.tables).flatMap((entry) => {
+ if (!isRecord(entry) || typeof entry.table !== 'string') return [];
+ const ref = parseDottedTableEntry(entry.table);
+ return ref ? [ref] : [];
+ });
+}
+
+function refsFromStandaloneSource(content: string): KtxTableRef[] {
+ const parsed = YAML.parse(content) as unknown;
+ if (!isRecord(parsed) || typeof parsed.table !== 'string') return [];
+ const ref = parseDottedTableEntry(parsed.table);
+ return ref ? [ref] : [];
+}
+
+async function semanticTableRefs(
+ projectDir: string,
+ connectionId: string,
+): Promise<{ refs: KtxTableRef[]; warnings: string[] }> {
+ const root = join(projectDir, 'semantic-layer', connectionId);
+ const files = await listYamlFiles(root);
+ const refs: KtxTableRef[] = [];
+ const warnings: string[] = [];
+ for (const file of files) {
+ try {
+ const content = await readFile(join(root, file), 'utf-8');
+ refs.push(...(file.startsWith('_schema/') ? refsFromManifest(content) : refsFromStandaloneSource(content)));
+ } catch (error) {
+ warnings.push(
+ `query_history_scope_floor_catalog_read_failed:${file}:${error instanceof Error ? error.message : String(error)}`,
+ );
+ }
+ }
+ return { refs: uniqueSortedTableRefs(refs), warnings };
+}
+
+export async function resolveQueryHistoryScopeFloor(input: QueryHistoryScopeFloorInput): Promise {
+ const explicitEnabledTables = [
+ ...tableRefsFromValues(input.storedQueryHistory.enabledTables),
+ ...tableRefsFromValues(input.connection.enabled_tables),
+ ];
+ const semanticTables = await semanticTableRefs(input.projectDir, input.connectionId);
+ const scannedTables = await scannedTableRefs(input.projectDir, input.connectionId);
+ const modeledTables = uniqueSortedTableRefs([
+ ...semanticTables.refs,
+ ...scannedTables.refs,
+ ...explicitEnabledTables,
+ ]);
+ const warnings = [...semanticTables.warnings, ...scannedTables.warnings];
+
+ if (explicitEnabledTables.length > 0) {
+ return {
+ enabledTables: explicitEnabledTables,
+ enabledTableKeys: tableRefSet(explicitEnabledTables),
+ enabledSchemas: [],
+ modeledTableCatalog: modeledTables,
+ floorDisabled: false,
+ warnings,
+ };
+ }
+
+ const explicitSchemas = stringArray(input.storedQueryHistory.enabledSchemas);
+ if (explicitSchemas.includes('*')) {
+ return {
+ enabledTables: [],
+ enabledTableKeys: null,
+ enabledSchemas: ['*'],
+ modeledTableCatalog: modeledTables,
+ floorDisabled: true,
+ warnings,
+ };
+ }
+ if (explicitSchemas.length > 0) {
+ if (!scannedTables.catalogAvailable || modeledTables.length === 0) {
+ return {
+ enabledTables: [],
+ enabledTableKeys: null,
+ enabledSchemas: ['*'],
+ modeledTableCatalog: modeledTables,
+ floorDisabled: true,
+ warnings: [...warnings, 'query_history_scope_floor_disabled:catalog_unavailable'],
+ };
+ }
+ return {
+ enabledTables: [],
+ enabledTableKeys: null,
+ enabledSchemas: [...new Set(explicitSchemas)].sort(),
+ modeledTableCatalog: modeledTables,
+ floorDisabled: false,
+ warnings,
+ };
+ }
+
+ const schemas = new Set(declaredSchemas(input.driver, input.connection));
+ for (const ref of semanticTables.refs) {
+ if (ref.db) schemas.add(ref.db);
+ }
+ if (schemas.size > 0 && (!scannedTables.catalogAvailable || modeledTables.length === 0)) {
+ return {
+ enabledTables: [],
+ enabledTableKeys: null,
+ enabledSchemas: ['*'],
+ modeledTableCatalog: modeledTables,
+ floorDisabled: true,
+ warnings: [...warnings, 'query_history_scope_floor_disabled:catalog_unavailable'],
+ };
+ }
+ return {
+ enabledTables: [],
+ enabledTableKeys: null,
+ enabledSchemas: [...schemas].sort(),
+ modeledTableCatalog: modeledTables,
+ floorDisabled: false,
+ warnings,
+ };
+}
diff --git a/packages/cli/src/context/ingest/adapters/historic-sql/scope-membership.ts b/packages/cli/src/context/ingest/adapters/historic-sql/scope-membership.ts
new file mode 100644
index 00000000..8852a82d
--- /dev/null
+++ b/packages/cli/src/context/ingest/adapters/historic-sql/scope-membership.ts
@@ -0,0 +1,45 @@
+import { tableRefKey, tableRefSet } from '../../../scan/table-ref.js';
+import type { KtxTableRef } from '../../../scan/types.js';
+
+export interface QueryHistoryScopeMembershipConfig {
+ enabledTables: readonly KtxTableRef[];
+ enabledSchemas: readonly string[];
+}
+
+function schemaNameForRef(ref: KtxTableRef): string | null {
+ return ref.db && ref.db.length > 0 ? ref.db : null;
+}
+
+function schemaNamesFromConfig(enabledSchemas: readonly string[]): Set {
+ return new Set(enabledSchemas.filter((schema) => schema !== '*'));
+}
+
+export function isQueryHistoryScopeFloorDisabled(config: QueryHistoryScopeMembershipConfig): boolean {
+ return config.enabledSchemas.includes('*');
+}
+
+export function shouldFailOpenQueryHistoryScope(config: QueryHistoryScopeMembershipConfig): boolean {
+ return (
+ config.enabledTables.length === 0 &&
+ !isQueryHistoryScopeFloorDisabled(config) &&
+ config.enabledSchemas.length === 0
+ );
+}
+
+export function includedQueryHistoryTableRefs(
+ tablesTouched: readonly KtxTableRef[],
+ config: QueryHistoryScopeMembershipConfig,
+): KtxTableRef[] {
+ if (config.enabledTables.length > 0) {
+ const enabled = tableRefSet(config.enabledTables);
+ return tablesTouched.filter((ref) => enabled.has(tableRefKey(ref)));
+ }
+ if (isQueryHistoryScopeFloorDisabled(config) || shouldFailOpenQueryHistoryScope(config)) {
+ return [...tablesTouched];
+ }
+ const schemas = schemaNamesFromConfig(config.enabledSchemas);
+ return tablesTouched.filter((ref) => {
+ const schema = schemaNameForRef(ref);
+ return schema !== null && schemas.has(schema);
+ });
+}
diff --git a/packages/cli/src/context/ingest/adapters/historic-sql/snowflake-query-history-reader.ts b/packages/cli/src/context/ingest/adapters/historic-sql/snowflake-query-history-reader.ts
index 539df3c3..65aafb12 100644
--- a/packages/cli/src/context/ingest/adapters/historic-sql/snowflake-query-history-reader.ts
+++ b/packages/cli/src/context/ingest/adapters/historic-sql/snowflake-query-history-reader.ts
@@ -188,26 +188,75 @@ export class SnowflakeHistoricSqlQueryHistoryReader {
config: HistoricSqlUnifiedPullConfig,
): AsyncIterable {
const sql = `
+WITH filtered_queries AS (
+ SELECT
+ query_hash,
+ query_text,
+ user_name,
+ start_time,
+ total_elapsed_time,
+ execution_status,
+ rows_produced
+ FROM SNOWFLAKE.ACCOUNT_USAGE.QUERY_HISTORY
+ WHERE query_text IS NOT NULL
+ AND query_type IN ('SELECT', 'MERGE')
+ AND start_time >= ${timestampLiteral(window.start)}
+ AND start_time < ${timestampLiteral(window.end)}
+),
+template_stats AS (
+ SELECT
+ query_hash AS template_id,
+ MIN(query_text) AS canonical_sql,
+ COUNT(*) AS executions,
+ COUNT(DISTINCT user_name) AS distinct_users,
+ MIN(start_time) AS first_seen,
+ MAX(start_time) AS last_seen,
+ APPROX_PERCENTILE(total_elapsed_time, 0.50) AS p50_ms,
+ APPROX_PERCENTILE(total_elapsed_time, 0.95) AS p95_ms,
+ DIV0(COUNT_IF(execution_status != 'SUCCESS'), COUNT(*)) AS error_rate,
+ SUM(rows_produced) AS rows_produced
+ FROM filtered_queries
+ GROUP BY query_hash
+ HAVING COUNT(*) >= ${config.minExecutions}
+),
+template_users AS (
+ SELECT
+ query_hash AS template_id,
+ user_name AS user,
+ COUNT(*) AS executions,
+ MAX(start_time) AS last_seen
+ FROM filtered_queries
+ GROUP BY query_hash, user_name
+)
SELECT
- query_hash AS template_id,
- MIN(query_text) AS canonical_sql,
- COUNT(*) AS executions,
- COUNT(DISTINCT user_name) AS distinct_users,
- MIN(start_time) AS first_seen,
- MAX(start_time) AS last_seen,
- APPROX_PERCENTILE(total_elapsed_time, 0.50) AS p50_ms,
- APPROX_PERCENTILE(total_elapsed_time, 0.95) AS p95_ms,
- DIV0(COUNT_IF(execution_status != 'SUCCESS'), COUNT(*)) AS error_rate,
- SUM(rows_produced) AS rows_produced,
- ARRAY_AGG(OBJECT_CONSTRUCT('user', user_name, 'executions', 1)) WITHIN GROUP (ORDER BY start_time DESC)::string AS top_users
-FROM SNOWFLAKE.ACCOUNT_USAGE.QUERY_HISTORY
-WHERE query_text IS NOT NULL
- AND query_type IN ('SELECT', 'MERGE')
- AND start_time >= ${timestampLiteral(window.start)}
- AND start_time < ${timestampLiteral(window.end)}
-GROUP BY query_hash
-HAVING COUNT(*) >= ${config.minExecutions}
-ORDER BY executions DESC`.trim();
+ stats.template_id,
+ stats.canonical_sql,
+ stats.executions,
+ stats.distinct_users,
+ stats.first_seen,
+ stats.last_seen,
+ stats.p50_ms,
+ stats.p95_ms,
+ stats.error_rate,
+ stats.rows_produced,
+ ARRAY_AGG(
+ OBJECT_CONSTRUCT('user', users.user, 'executions', users.executions)
+ ) WITHIN GROUP (ORDER BY users.executions DESC, users.last_seen DESC)::string AS top_users
+FROM template_stats AS stats
+JOIN template_users AS users
+ ON users.template_id = stats.template_id
+GROUP BY
+ stats.template_id,
+ stats.canonical_sql,
+ stats.executions,
+ stats.distinct_users,
+ stats.first_seen,
+ stats.last_seen,
+ stats.p50_ms,
+ stats.p95_ms,
+ stats.error_rate,
+ stats.rows_produced
+ORDER BY stats.executions DESC`.trim();
const result = await queryClient(client).executeQuery(sql);
if (result.error) {
throw grantsError(result.error);
diff --git a/packages/cli/src/context/ingest/adapters/historic-sql/stage-unified.ts b/packages/cli/src/context/ingest/adapters/historic-sql/stage-unified.ts
index 853a3e68..84ec75a7 100644
--- a/packages/cli/src/context/ingest/adapters/historic-sql/stage-unified.ts
+++ b/packages/cli/src/context/ingest/adapters/historic-sql/stage-unified.ts
@@ -1,6 +1,8 @@
import { mkdir, writeFile } from 'node:fs/promises';
import { dirname, join } from 'node:path';
import type { SqlAnalysisPort } from '../../../../context/sql-analysis/ports.js';
+import { tableRefKey, type KtxTableRefKey } from '../../../scan/table-ref.js';
+import type { KtxTableRef } from '../../../scan/types.js';
import {
bucketDistinctUsers,
bucketErrorRate,
@@ -15,6 +17,11 @@ import {
redactHistoricSqlText,
type HistoricSqlRedactionPattern,
} from './redaction.js';
+import {
+ includedQueryHistoryTableRefs,
+ isQueryHistoryScopeFloorDisabled,
+ shouldFailOpenQueryHistoryScope,
+} from './scope-membership.js';
import {
HISTORIC_SQL_SOURCE_KEY,
aggregatedTemplateSchema,
@@ -38,17 +45,13 @@ interface StageHistoricSqlAggregatedSnapshotInput {
interface ParsedTemplate {
template: AggregatedTemplate;
- tablesTouched: string[];
- includedTables: string[];
+ tablesTouched: KtxTableRef[];
+ includedTables: KtxTableRef[];
columnsByClause: Record;
}
-interface EnabledTableFilter {
- exact: Set;
- uniqueUnqualified: Set;
-}
-
interface TableAccumulator {
+ tableRef: KtxTableRef;
table: string;
executions: number;
distinctUsers: number;
@@ -105,8 +108,7 @@ function shouldDropByUsers(template: AggregatedTemplate, config: HistoricSqlUnif
const matchingExecutions = template.topUsers
.filter((entry) => matchesAny(entry.user, patterns))
.reduce((sum, entry) => sum + entry.executions, 0);
- const allExecutions = template.topUsers.reduce((sum, entry) => sum + entry.executions, 0);
- const serviceOnly = allExecutions > 0 && matchingExecutions >= allExecutions;
+ const serviceOnly = template.stats.executions > 0 && matchingExecutions >= template.stats.executions;
return service.mode === 'exclude' ? serviceOnly : !serviceOnly;
}
@@ -122,90 +124,8 @@ function shouldDropTemplate(template: AggregatedTemplate, config: HistoricSqlUni
return false;
}
-function normalizeTableIdentifier(value: string): string {
- return value.trim().toLowerCase();
-}
-
-function unqualifiedTableIdentifier(value: string): string {
- const parts = normalizeTableIdentifier(value).split('.').filter(Boolean);
- return parts.at(-1) ?? '';
-}
-
-function buildEnabledTableFilter(enabledTables: string[]): EnabledTableFilter | null {
- if (enabledTables.length === 0) {
- return null;
- }
- const exact = new Set(enabledTables.map(normalizeTableIdentifier).filter((value) => value.length > 0));
- const unqualifiedCounts = new Map();
- for (const table of exact) {
- const unqualified = unqualifiedTableIdentifier(table);
- if (unqualified.length > 0) {
- unqualifiedCounts.set(unqualified, (unqualifiedCounts.get(unqualified) ?? 0) + 1);
- }
- }
- return {
- exact,
- uniqueUnqualified: new Set(
- [...unqualifiedCounts.entries()]
- .filter(([, count]) => count === 1)
- .map(([table]) => table),
- ),
- };
-}
-
-function isEnabledTable(table: string, filter: EnabledTableFilter | null): boolean {
- if (!filter) {
- return true;
- }
- const normalized = normalizeTableIdentifier(table);
- return filter.exact.has(normalized) || filter.uniqueUnqualified.has(unqualifiedTableIdentifier(normalized));
-}
-
-/**
- * pg_stat_statements records queries as written, so the same physical table can appear
- * both bare (`accounts`, resolved via search_path) and schema-qualified
- * (`orbit_raw.accounts`). Collapse a bare identifier into its schema-qualified form when
- * exactly one qualified form shares its unqualified name, so the two never become separate
- * work units. Ambiguous bare names (two qualified forms) are left untouched.
- */
-function canonicalizeTableIdentifiers(parsedTemplates: ParsedTemplate[]): void {
- const all = new Set();
- for (const parsed of parsedTemplates) {
- for (const table of parsed.includedTables) {
- all.add(table);
- }
- }
- const qualifiedByUnqualified = new Map>();
- for (const table of all) {
- if (!table.includes('.')) {
- continue;
- }
- const unqualified = unqualifiedTableIdentifier(table);
- if (unqualified.length === 0) {
- continue;
- }
- const forms = qualifiedByUnqualified.get(unqualified) ?? new Set();
- forms.add(table);
- qualifiedByUnqualified.set(unqualified, forms);
- }
- const canonical = new Map();
- for (const table of all) {
- if (table.includes('.')) {
- continue;
- }
- const forms = qualifiedByUnqualified.get(unqualifiedTableIdentifier(table));
- if (forms && forms.size === 1) {
- canonical.set(table, [...forms][0]);
- }
- }
- if (canonical.size === 0) {
- return;
- }
- const remap = (table: string): string => canonical.get(table) ?? table;
- for (const parsed of parsedTemplates) {
- parsed.includedTables = [...new Set(parsed.includedTables.map(remap))].sort();
- parsed.tablesTouched = [...new Set(parsed.tablesTouched.map(remap))].sort();
- }
+function displayTableRef(ref: KtxTableRef): string {
+ return [ref.catalog, ref.db, ref.name].filter((part): part is string => !!part && part.length > 0).join('.');
}
function historicSqlWindowDays(config: HistoricSqlUnifiedPullConfig): number {
@@ -240,9 +160,10 @@ function recordJoin(acc: TableAccumulator, otherTable: string, columns: string[]
}
}
-function accumulatorFor(table: string): TableAccumulator {
+function accumulatorFor(tableRef: KtxTableRef): TableAccumulator {
return {
- table,
+ tableRef,
+ table: displayTableRef(tableRef),
executions: 0,
distinctUsers: 0,
errorRateNumerator: 0,
@@ -272,8 +193,8 @@ function addTemplate(acc: TableAccumulator, parsed: ParsedTemplate): void {
}
}
const joinColumns = parsed.columnsByClause.join ?? [];
- for (const otherTable of parsed.tablesTouched.filter((table) => table !== acc.table)) {
- recordJoin(acc, otherTable, joinColumns, executions);
+ for (const otherTable of parsed.tablesTouched.filter((table) => tableRefKey(table) !== tableRefKey(acc.tableRef))) {
+ recordJoin(acc, displayTableRef(otherTable), joinColumns, executions);
}
acc.topTemplates.push(parsed.template);
}
@@ -310,6 +231,7 @@ function toStagedTable(acc: TableAccumulator, now: Date): StagedTableInput {
return {
table: acc.table,
+ tableRef: acc.tableRef,
stats: {
executionsBucket: bucketExecutions(acc.executions),
distinctUsersBucket: bucketDistinctUsers(acc.distinctUsers),
@@ -329,7 +251,7 @@ function toPatternsInput(parsedTemplates: ParsedTemplate[]): StagedPatternsInput
.map(({ template, tablesTouched }) => ({
id: template.templateId,
canonicalSql: template.canonicalSql,
- tablesTouched: [...tablesTouched].sort(),
+ tablesTouched: [...tablesTouched].sort((left, right) => tableRefKey(left).localeCompare(tableRefKey(right))),
executionsBucket: bucketExecutions(template.stats.executions),
distinctUsersBucket: bucketDistinctUsers(template.stats.distinctUsers),
dialect: template.dialect,
@@ -340,7 +262,6 @@ function toPatternsInput(parsedTemplates: ParsedTemplate[]): StagedPatternsInput
export async function stageHistoricSqlAggregatedSnapshot(input: StageHistoricSqlAggregatedSnapshotInput): Promise {
const config = historicSqlUnifiedPullConfigSchema.parse(input.pullConfig);
- const enabledTableFilter = buildEnabledTableFilter(config.enabledTables);
const redactors = compileHistoricSqlRedactionPatterns(config.redactionPatterns);
const now = input.now ?? new Date();
const windowStart = new Date(now.getTime() - historicSqlWindowDays(config) * 24 * 60 * 60 * 1000);
@@ -356,11 +277,25 @@ export async function stageHistoricSqlAggregatedSnapshot(input: StageHistoricSql
}
}
- const analysis = await input.sqlAnalysis.analyzeBatch(
- snapshot.map((template) => ({ id: template.templateId, sql: template.canonicalSql })),
- config.dialect,
- );
- const warnings: string[] = [];
+ const analysisItems = snapshot.map((template) => ({ id: template.templateId, sql: template.canonicalSql }));
+ const analysisOptions =
+ config.modeledTableCatalog.length > 0 ? { catalog: { tables: config.modeledTableCatalog } } : undefined;
+ const warnings: string[] = [
+ ...config.scopeFloorWarnings,
+ ...(shouldFailOpenQueryHistoryScope(config) ? ['query_history_scope_floor_disabled:empty_modeled_scope'] : []),
+ ];
+ let scopeDisabledByQualificationFailure = false;
+ let analysis: Awaited>;
+ try {
+ analysis = await input.sqlAnalysis.analyzeBatch(analysisItems, config.dialect, analysisOptions);
+ } catch (error) {
+ if (!analysisOptions || config.enabledTables.length > 0 || isQueryHistoryScopeFloorDisabled(config)) {
+ throw error;
+ }
+ warnings.push('query_history_scope_floor_disabled:catalog_qualification_failed');
+ scopeDisabledByQualificationFailure = true;
+ analysis = await input.sqlAnalysis.analyzeBatch(analysisItems, config.dialect, undefined);
+ }
const parsedTemplates: ParsedTemplate[] = [];
for (const template of snapshot) {
const parsed = analysis.get(template.templateId);
@@ -368,8 +303,12 @@ export async function stageHistoricSqlAggregatedSnapshot(input: StageHistoricSql
warnings.push(`parse_failed:${template.templateId}`);
continue;
}
- const tablesTouched = [...new Set(parsed.tablesTouched)].filter((table) => table.length > 0).sort();
- const includedTables = tablesTouched.filter((table) => isEnabledTable(table, enabledTableFilter));
+ const tablesTouched = [...new Map(parsed.tablesTouched.map((ref) => [tableRefKey(ref), ref])).values()]
+ .filter((ref) => ref.name.length > 0)
+ .sort((left, right) => tableRefKey(left).localeCompare(tableRefKey(right)));
+ const includedTables = scopeDisabledByQualificationFailure
+ ? [...tablesTouched]
+ : includedQueryHistoryTableRefs(tablesTouched, config);
if (includedTables.length === 0) {
continue;
}
@@ -383,24 +322,23 @@ export async function stageHistoricSqlAggregatedSnapshot(input: StageHistoricSql
});
}
- canonicalizeTableIdentifiers(parsedTemplates);
-
- const byTable = new Map();
+ const byTable = new Map();
for (const parsed of parsedTemplates) {
- for (const table of parsed.includedTables) {
- const acc = byTable.get(table) ?? accumulatorFor(table);
+ for (const tableRef of parsed.includedTables) {
+ const key = tableRefKey(tableRef);
+ const acc = byTable.get(key) ?? accumulatorFor(tableRef);
addTemplate(acc, parsed);
- byTable.set(table, acc);
+ byTable.set(key, acc);
}
}
await mkdir(input.stagedDir, { recursive: true });
- for (const [table, acc] of [...byTable.entries()].sort(([left], [right]) => left.localeCompare(right))) {
- await writeJson(input.stagedDir, `tables/${table}.json`, toStagedTable(acc, now));
+ for (const [, acc] of [...byTable.entries()].sort((left, right) => left[0].localeCompare(right[0]))) {
+ await writeJson(input.stagedDir, `tables/${acc.table}.json`, toStagedTable(acc, now));
}
const patternsInput = toPatternsInput(parsedTemplates);
const patternInputSplit = splitHistoricSqlPatternInputs(patternsInput);
- const allWarnings = [...warnings, ...patternInputSplit.warnings];
+ const allWarnings = [...new Set([...warnings, ...patternInputSplit.warnings])];
await writeJson(input.stagedDir, 'patterns-input.json', patternInputSplit.auditInput);
for (const shard of patternInputSplit.shards) {
await writeJson(input.stagedDir, shard.path, shard.input);
diff --git a/packages/cli/src/context/ingest/adapters/historic-sql/types.ts b/packages/cli/src/context/ingest/adapters/historic-sql/types.ts
index 1d256b13..aca50c4e 100644
--- a/packages/cli/src/context/ingest/adapters/historic-sql/types.ts
+++ b/packages/cli/src/context/ingest/adapters/historic-sql/types.ts
@@ -8,9 +8,22 @@ export type HistoricSqlDialect = z.infer;
const filterModeSchema = z.enum(['exclude', 'include', 'mark-only']);
+const ktxTableRefSchema = z.object({
+ catalog: z.string().nullable(),
+ db: z.string().nullable(),
+ name: z.string().min(1),
+}).strict();
+
+const ktxTableRefWithColumnsSchema = ktxTableRefSchema.extend({
+ columns: z.array(z.string().min(1)).optional(),
+}).strict();
+
const historicSqlCommonPullConfigSchema = z.object({
minExecutions: z.number().int().nonnegative().default(5),
- enabledTables: z.array(z.string().min(1)).default([]),
+ enabledTables: z.array(ktxTableRefSchema).default([]),
+ enabledSchemas: z.array(z.string().min(1)).default([]),
+ modeledTableCatalog: z.array(ktxTableRefWithColumnsSchema).default([]),
+ scopeFloorWarnings: z.array(z.string()).default([]),
filters: z.object({
serviceAccounts: z.object({
patterns: z.array(z.string()).default([]),
@@ -68,6 +81,7 @@ export type AggregatedTemplate = z.infer;
export const stagedTableInputSchema = z.object({
table: z.string().min(1),
+ tableRef: ktxTableRefSchema,
stats: z.object({
executionsBucket: z.string(),
distinctUsersBucket: z.string(),
@@ -93,7 +107,7 @@ export const stagedPatternsInputSchema = z.object({
templates: z.array(z.object({
id: z.string(),
canonicalSql: z.string(),
- tablesTouched: z.array(z.string()),
+ tablesTouched: z.array(ktxTableRefSchema),
executionsBucket: z.string(),
distinctUsersBucket: z.string(),
dialect: historicSqlDialectSchema,
diff --git a/packages/cli/src/context/ingest/local-adapters.ts b/packages/cli/src/context/ingest/local-adapters.ts
index 4739f4e4..3cd8a998 100644
--- a/packages/cli/src/context/ingest/local-adapters.ts
+++ b/packages/cli/src/context/ingest/local-adapters.ts
@@ -9,6 +9,7 @@ import { DbtSourceAdapter } from './adapters/dbt/dbt.adapter.js';
import { FakeSourceAdapter } from './adapters/fake/fake.adapter.js';
import { HistoricSqlSourceAdapter } from './adapters/historic-sql/historic-sql.adapter.js';
import { PostgresPgssReader } from './adapters/historic-sql/postgres-pgss-reader.js';
+import { resolveQueryHistoryScopeFloor } from './adapters/historic-sql/scope-floor.js';
import {
HISTORIC_SQL_SOURCE_KEY,
historicSqlUnifiedPullConfigSchema,
@@ -179,12 +180,39 @@ function queryHistoryRecord(connection: unknown): Record | null
return queryHistory;
}
-function queryHistoryPullConfig(connection: unknown): Record | null {
+async function queryHistoryPullConfig(
+ project: KtxLocalProject,
+ connectionId: string,
+ connection: unknown,
+): Promise | null> {
const queryHistory = queryHistoryRecord(connection);
if (queryHistory?.enabled !== true || !isRecord(connection)) return null;
- const dialect = historicSqlDialectByDriver.get(String(connection.driver ?? '').toLowerCase());
+ const driver = String(connection.driver ?? '').toLowerCase();
+ const dialect = historicSqlDialectByDriver.get(driver);
if (!dialect) return null;
- return { ...queryHistory, dialect };
+ const scopeFloor = await resolveQueryHistoryScopeFloor({
+ projectDir: project.projectDir,
+ connectionId,
+ driver,
+ connection,
+ storedQueryHistory: queryHistory,
+ });
+ const {
+ enabled: _enabled,
+ dialect: _dialect,
+ enabledTables: _enabledTables,
+ enabledSchemas: _enabledSchemas,
+ scopeFloorWarnings: _scopeFloorWarnings,
+ ...stored
+ } = queryHistory;
+ return {
+ ...stored,
+ dialect,
+ ...(scopeFloor.enabledTables.length > 0 ? { enabledTables: scopeFloor.enabledTables } : {}),
+ ...(scopeFloor.enabledSchemas.length > 0 ? { enabledSchemas: scopeFloor.enabledSchemas } : {}),
+ ...(scopeFloor.modeledTableCatalog.length > 0 ? { modeledTableCatalog: scopeFloor.modeledTableCatalog } : {}),
+ ...(scopeFloor.warnings.length > 0 ? { scopeFloorWarnings: scopeFloor.warnings } : {}),
+ };
}
function stringField(value: unknown): string | null {
@@ -245,7 +273,7 @@ export async function localPullConfigForAdapter(
if (options.historicSqlPullConfigOverride) {
return historicSqlUnifiedPullConfigSchema.parse(options.historicSqlPullConfigOverride);
}
- const queryHistory = queryHistoryPullConfig(connection);
+ const queryHistory = await queryHistoryPullConfig(project, connectionId, connection);
if (!queryHistory) {
throw new Error(`Connection "${connectionId}" does not have context.queryHistory.enabled: true`);
}
diff --git a/packages/cli/src/context/sql-analysis/http-sql-analysis-port.ts b/packages/cli/src/context/sql-analysis/http-sql-analysis-port.ts
index 238b8863..3093816d 100644
--- a/packages/cli/src/context/sql-analysis/http-sql-analysis-port.ts
+++ b/packages/cli/src/context/sql-analysis/http-sql-analysis-port.ts
@@ -1,8 +1,10 @@
import { request as httpRequest } from 'node:http';
import { request as httpsRequest } from 'node:https';
import { URL } from 'node:url';
+import type { KtxTableRef } from '../scan/types.js';
import type {
SqlAnalysisBatchItem,
+ SqlAnalysisBatchOptions,
SqlAnalysisBatchResult,
SqlAnalysisDialect,
SqlAnalysisFingerprintResult,
@@ -89,6 +91,14 @@ function optionalString(raw: Record, field: string): string | n
throw new Error(`sql analysis response has invalid optional string field ${field}`);
}
+function optionalNullableStringField(raw: Record, field: string): string | null {
+ const value = raw[field];
+ if (value === null || value === undefined || typeof value === 'string') {
+ return value ?? null;
+ }
+ throw new Error(`sql analysis response has invalid optional nullable string field ${field}`);
+}
+
function requiredStringArray(raw: Record, field: string): string[] {
const value = raw[field];
if (!Array.isArray(value) || value.some((item) => typeof item !== 'string')) {
@@ -175,10 +185,34 @@ function mapColumnsByClause(raw: Record): SqlAnalysisBatchResul
return result;
}
+function requiredTableRef(raw: unknown, field: string): KtxTableRef {
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
+ throw new Error(`sql analysis response contains invalid table ref in ${field}`);
+ }
+ const record = raw as Record;
+ const name = record.name;
+ if (typeof name !== 'string' || name.length === 0) {
+ throw new Error(`sql analysis response table ref in ${field} is missing name`);
+ }
+ return {
+ catalog: optionalNullableStringField(record, 'catalog'),
+ db: optionalNullableStringField(record, 'db'),
+ name,
+ };
+}
+
+function requiredTableRefArray(raw: Record, field: string): KtxTableRef[] {
+ const value = raw[field];
+ if (!Array.isArray(value)) {
+ throw new Error(`sql analysis response is missing table-ref[] field ${field}`);
+ }
+ return value.map((item, index) => requiredTableRef(item, `${field}.${index}`));
+}
+
function mapBatchResult(raw: Record): SqlAnalysisBatchResult {
const error = optionalString(raw, 'error');
return {
- tablesTouched: requiredStringArray(raw, 'tables_touched'),
+ tablesTouched: requiredTableRefArray(raw, 'tables_touched'),
columnsByClause: mapColumnsByClause(raw),
...(error !== undefined ? { error } : {}),
};
@@ -215,10 +249,11 @@ export function createHttpSqlAnalysisPort(options: HttpSqlAnalysisPortOptions):
});
return mapResult(raw);
},
- async analyzeBatch(items: SqlAnalysisBatchItem[], dialect: SqlAnalysisDialect) {
+ async analyzeBatch(items: SqlAnalysisBatchItem[], dialect: SqlAnalysisDialect, options?: SqlAnalysisBatchOptions) {
const raw = await requestJson('/sql/analyze-batch', {
dialect,
items,
+ ...(options?.catalog ? { catalog: options.catalog } : {}),
});
return mapBatchResponse(raw);
},
diff --git a/packages/cli/src/context/sql-analysis/ports.ts b/packages/cli/src/context/sql-analysis/ports.ts
index 887be605..898fca38 100644
--- a/packages/cli/src/context/sql-analysis/ports.ts
+++ b/packages/cli/src/context/sql-analysis/ports.ts
@@ -1,3 +1,5 @@
+import type { KtxTableRef } from '../scan/types.js';
+
export type SqlAnalysisDialect =
| 'bigquery'
| 'snowflake'
@@ -32,8 +34,20 @@ export interface SqlAnalysisBatchItem {
sql: string;
}
+interface SqlAnalysisCatalogTable extends KtxTableRef {
+ columns?: string[];
+}
+
+interface SqlAnalysisCatalog {
+ tables: SqlAnalysisCatalogTable[];
+}
+
+export interface SqlAnalysisBatchOptions {
+ catalog?: SqlAnalysisCatalog;
+}
+
export interface SqlAnalysisBatchResult {
- tablesTouched: string[];
+ tablesTouched: KtxTableRef[];
columnsByClause: Partial>;
error?: string | null;
}
@@ -48,6 +62,7 @@ export interface SqlAnalysisPort {
analyzeBatch(
items: SqlAnalysisBatchItem[],
dialect: SqlAnalysisDialect,
+ options?: SqlAnalysisBatchOptions,
): Promise>;
validateReadOnly(sql: string, dialect: SqlAnalysisDialect): Promise;
}
diff --git a/packages/cli/src/local-adapters.ts b/packages/cli/src/local-adapters.ts
index 0cd2d940..3e3b0486 100644
--- a/packages/cli/src/local-adapters.ts
+++ b/packages/cli/src/local-adapters.ts
@@ -15,7 +15,7 @@ import { BigQueryHistoricSqlQueryHistoryReader } from './context/ingest/adapters
import { historicSqlDialectForConnectionDriver } from './context/ingest/adapters/historic-sql/connection-dialect.js';
import { createDaemonLiveDatabaseIntrospection } from './context/ingest/adapters/live-database/daemon-introspection.js';
import { createDefaultLocalIngestAdapters, type DefaultLocalIngestAdaptersOptions } from './context/ingest/local-adapters.js';
-import type { HistoricSqlReader } from './context/ingest/adapters/historic-sql/types.js';
+import type { HistoricSqlDialect, HistoricSqlReader } from './context/ingest/adapters/historic-sql/types.js';
import type {
LiveDatabaseIntrospectionOptions,
LiveDatabaseIntrospectionPort,
@@ -31,7 +31,7 @@ import {
createManagedDaemonLookerTableIdentifierParser,
createManagedDaemonSqlAnalysisPort,
managedDaemonDatabaseIntrospectionOptions,
- type ManagedPythonCoreDaemonOptions,
+ type ManagedPythonDaemonHttpOptions,
} from './managed-python-http.js';
import type { KtxOperationalLogger } from './io/logger.js';
import { resolveKtxConfigReference } from './context/core/config-reference.js';
@@ -161,10 +161,17 @@ export interface KtxCliLocalIngestAdaptersOptions extends DefaultLocalIngestAdap
historicSqlConnectionId?: string;
sqlAnalysis?: SqlAnalysisPort;
sqlAnalysisUrl?: string;
- managedDaemon?: ManagedPythonCoreDaemonOptions;
+ managedDaemon?: ManagedPythonDaemonHttpOptions;
logger?: KtxOperationalLogger;
}
+export interface KtxCliHistoricSqlRuntime {
+ dialect: HistoricSqlDialect;
+ sqlAnalysis: SqlAnalysisPort;
+ reader: HistoricSqlReader;
+ queryClient: unknown;
+}
+
function createEphemeralPostgresHistoricSqlClient(project: KtxLocalProject, connectionId: string) {
const connection = project.config.connections[connectionId] as KtxPostgresConnectionConfig | undefined;
const inputDriver = connection?.driver ?? 'unknown';
@@ -262,7 +269,10 @@ function bigQueryRegion(connection: KtxBigQueryConnectionConfig): string {
: 'us';
}
-function historicSqlOptionsForLocalRun(project: KtxLocalProject, options: KtxCliLocalIngestAdaptersOptions) {
+function historicSqlOptionsForLocalRun(
+ project: KtxLocalProject,
+ options: KtxCliLocalIngestAdaptersOptions,
+): KtxCliHistoricSqlRuntime | undefined {
const connectionId = options.historicSqlConnectionId;
if (!connectionId) {
return undefined;
@@ -285,6 +295,7 @@ function historicSqlOptionsForLocalRun(project: KtxLocalProject, options: KtxCli
if (dialect === 'postgres') {
return {
...base,
+ dialect,
reader: new PostgresPgssReader() satisfies HistoricSqlReader,
queryClient: createEphemeralPostgresHistoricSqlClient(project, connectionId),
};
@@ -297,6 +308,7 @@ function historicSqlOptionsForLocalRun(project: KtxLocalProject, options: KtxCli
}
return {
...base,
+ dialect,
reader: new BigQueryHistoricSqlQueryHistoryReader({
projectId: bigQueryProjectId(connection, process.env),
region: bigQueryRegion(connection),
@@ -307,6 +319,7 @@ function historicSqlOptionsForLocalRun(project: KtxLocalProject, options: KtxCli
return {
...base,
+ dialect,
reader: new SnowflakeHistoricSqlQueryHistoryReader() satisfies HistoricSqlReader,
queryClient: {
async executeQuery(query: string) {
@@ -318,11 +331,24 @@ function historicSqlOptionsForLocalRun(project: KtxLocalProject, options: KtxCli
};
}
+export function createKtxCliHistoricSqlRuntime(
+ project: KtxLocalProject,
+ connectionId: string,
+ options: KtxCliLocalIngestAdaptersOptions = {},
+): KtxCliHistoricSqlRuntime | undefined {
+ return historicSqlOptionsForLocalRun(project, {
+ ...options,
+ historicSqlConnectionId: connectionId,
+ });
+}
+
export function createKtxCliLocalIngestAdapters(
project: KtxLocalProject,
options: KtxCliLocalIngestAdaptersOptions = {},
): SourceAdapter[] {
- const historicSql = historicSqlOptionsForLocalRun(project, options);
+ const historicSql = options.historicSqlConnectionId
+ ? createKtxCliHistoricSqlRuntime(project, options.historicSqlConnectionId, options)
+ : undefined;
const base = createDefaultLocalIngestAdapters(project, {
...options,
databaseIntrospection: ktxCliDaemonDatabaseIntrospectionOptions(options),
diff --git a/packages/cli/src/public-ingest.ts b/packages/cli/src/public-ingest.ts
index 7fc43ac4..44a2b024 100644
--- a/packages/cli/src/public-ingest.ts
+++ b/packages/cli/src/public-ingest.ts
@@ -5,6 +5,7 @@ import type { KtxProgressPort } from './context/scan/types.js';
import type { KtxCliIo } from './index.js';
import type { KtxIngestArgs, KtxIngestDeps, KtxIngestProgressUpdate } from './ingest.js';
import { isDatabaseDriver, normalizeConnectionDriver } from './connection-drivers.js';
+import { resolveQueryHistoryScopeFloor } from './context/ingest/adapters/historic-sql/scope-floor.js';
import {
ensureManagedPythonCommandRuntime,
type KtxManagedPythonInstallPolicy,
@@ -19,6 +20,7 @@ import {
import { createAggregateProgressPort } from './progress-port-adapter.js';
import { resolvePublicIngestRuntimeRequirements } from './runtime-requirements.js';
import type { KtxScanArgs, KtxScanDeps } from './scan.js';
+import type { KtxTableRef } from './context/scan/types.js';
import { profileMark } from './startup-profile.js';
import { isDemoConnection } from './telemetry/demo-detect.js';
import { emitProjectStackSnapshot, emitTelemetryEvent } from './telemetry/index.js';
@@ -281,26 +283,35 @@ function positiveInteger(value: unknown): number | undefined {
return typeof value === 'number' && Number.isInteger(value) && value > 0 ? value : undefined;
}
-function enabledTablesForConnection(connection: KtxProjectConnectionConfig): string[] | undefined {
- const raw = connection.enabled_tables;
- if (!Array.isArray(raw)) {
- return undefined;
- }
- const tables = raw.filter((value): value is string => typeof value === 'string' && value.trim().length > 0);
- return tables.length > 0 ? tables : undefined;
-}
-
-function queryHistoryPullConfig(input: {
+/** @internal */
+export function queryHistoryPullConfig(input: {
stored: Record;
dialect: HistoricSqlDialect;
windowDays?: number;
- enabledTables?: string[];
+ enabledTables?: KtxTableRef[];
+ enabledSchemas?: string[];
+ modeledTableCatalog?: KtxTableRef[];
+ scopeFloorWarnings?: string[];
}): Record {
- const { enabled: _enabled, dialect: _dialect, ...storedConfig } = input.stored;
+ const {
+ enabled: _enabled,
+ dialect: _dialect,
+ enabledTables: _enabledTables,
+ enabledSchemas: _enabledSchemas,
+ scopeFloorWarnings: _scopeFloorWarnings,
+ ...storedConfig
+ } = input.stored;
return {
...storedConfig,
dialect: input.dialect,
- ...(input.enabledTables ? { enabledTables: input.enabledTables } : {}),
+ ...(input.enabledTables && input.enabledTables.length > 0 ? { enabledTables: input.enabledTables } : {}),
+ ...(input.enabledSchemas && input.enabledSchemas.length > 0 ? { enabledSchemas: input.enabledSchemas } : {}),
+ ...(input.modeledTableCatalog && input.modeledTableCatalog.length > 0
+ ? { modeledTableCatalog: input.modeledTableCatalog }
+ : {}),
+ ...(input.scopeFloorWarnings && input.scopeFloorWarnings.length > 0
+ ? { scopeFloorWarnings: input.scopeFloorWarnings }
+ : {}),
...(input.windowDays !== undefined ? { windowDays: input.windowDays } : {}),
};
}
@@ -361,7 +372,6 @@ function resolveDatabaseTargetOptions(input: {
stored: storedQh,
dialect,
windowDays: queryHistory.windowDays,
- enabledTables: enabledTablesForConnection(input.connection),
}),
},
steps: ['database-schema', 'query-history'],
@@ -374,6 +384,43 @@ function resolveDatabaseTargetOptions(input: {
};
}
+async function resolvedQueryHistoryPullConfigForTarget(
+ target: KtxPublicIngestPlanTarget,
+ project: KtxPublicIngestProject,
+): Promise | null> {
+ if (target.operation !== 'database-ingest' || target.queryHistory?.enabled !== true || !target.queryHistory.dialect) {
+ return null;
+ }
+ const connection = project.config.connections[target.connectionId];
+ if (!connection) {
+ return (
+ target.queryHistory.pullConfig ??
+ queryHistoryPullConfig({
+ stored: {},
+ dialect: target.queryHistory.dialect,
+ windowDays: target.queryHistory.windowDays,
+ })
+ );
+ }
+ const stored = storedQueryHistory(connection);
+ const scopeFloor = await resolveQueryHistoryScopeFloor({
+ projectDir: project.projectDir,
+ connectionId: target.connectionId,
+ driver: target.driver,
+ connection: connection as Record,
+ storedQueryHistory: stored,
+ });
+ return queryHistoryPullConfig({
+ stored,
+ dialect: target.queryHistory.dialect,
+ windowDays: target.queryHistory.windowDays,
+ enabledTables: scopeFloor.enabledTables,
+ enabledSchemas: scopeFloor.enabledSchemas,
+ modeledTableCatalog: scopeFloor.modeledTableCatalog,
+ scopeFloorWarnings: scopeFloor.warnings,
+ });
+}
+
function enrichmentReadinessGaps(config: KtxProjectConfig): string[] {
const gaps: string[] = [];
if (config.llm.provider.backend === 'none' || !config.llm.models.default) {
@@ -877,7 +924,7 @@ export async function executePublicIngestTarget(
project: KtxPublicIngestProject,
): Promise {
const startedAt = performance.now();
- const result = await runIngestTargetSteps(target, args, io, deps);
+ const result = await runIngestTargetSteps(target, args, io, deps, project);
// `io` may be a capture buffer for the scan/ingest step output; the telemetry
// debug echo belongs on the real user-facing stream, which callers expose as
// `deps.runtimeIo` (falling back to `io` when the step io is already real).
@@ -890,6 +937,7 @@ async function runIngestTargetSteps(
args: Extract,
io: KtxCliIo,
deps: KtxPublicIngestDeps,
+ project: KtxPublicIngestProject,
): Promise {
if (target.preflightFailure) {
if (target.operation === 'database-ingest') {
@@ -959,6 +1007,11 @@ async function runIngestTargetSteps(
if (target.queryHistory?.enabled === true) {
const { runKtxIngest } = await import('./ingest.js');
const runIngest = deps.runIngest ?? runKtxIngest;
+ const historicSqlPullConfigOverride =
+ (await resolvedQueryHistoryPullConfigForTarget(target, project)) ?? {
+ dialect: target.queryHistory.dialect,
+ ...(target.queryHistory.windowDays !== undefined ? { windowDays: target.queryHistory.windowDays } : {}),
+ };
const ingestArgs: KtxIngestArgs = {
command: 'run',
projectDir: args.projectDir,
@@ -969,11 +1022,7 @@ async function runIngestTargetSteps(
...(args.cliVersion ? { cliVersion: args.cliVersion } : {}),
...(args.runtimeInstallPolicy ? { runtimeInstallPolicy: args.runtimeInstallPolicy } : {}),
allowImplicitAdapter: true,
- historicSqlPullConfigOverride:
- target.queryHistory.pullConfig ?? {
- dialect: target.queryHistory.dialect,
- ...(target.queryHistory.windowDays !== undefined ? { windowDays: target.queryHistory.windowDays } : {}),
- },
+ historicSqlPullConfigOverride,
};
// Query history runs after the schema scan has already written its report
// into the shared target io, so it needs a phase-local capture. Reusing
diff --git a/packages/cli/src/setup-databases.ts b/packages/cli/src/setup-databases.ts
index 1fd93486..3cb6c5d2 100644
--- a/packages/cli/src/setup-databases.ts
+++ b/packages/cli/src/setup-databases.ts
@@ -4,7 +4,15 @@ import { delimiter, dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { promisify } from 'node:util';
import { getDriverRegistration } from './context/connections/drivers.js';
+import { createLocalKtxLlmRuntimeFromConfig } from './context/llm/local-config.js';
+import type { KtxLlmRuntimePort } from './context/llm/runtime-port.js';
import { queryHistoryDialectForConnection } from './context/ingest/adapters/historic-sql/connection-dialect.js';
+import {
+ proposeQueryHistoryServiceAccountFilters,
+ type ProposeQueryHistoryServiceAccountFiltersInput,
+ type QueryHistoryFilterProposal,
+} from './context/ingest/adapters/historic-sql/query-history-filter-picker.js';
+import { resolveQueryHistoryScopeFloor } from './context/ingest/adapters/historic-sql/scope-floor.js';
import type { HistoricSqlDialect } from './context/ingest/adapters/historic-sql/types.js';
import {
runHistoricSqlReadinessProbe,
@@ -15,7 +23,7 @@ import { type KtxProjectConnectionConfig, serializeKtxProjectConfig } from './co
import { loadKtxProject } from './context/project/project.js';
import { markKtxSetupStateStepComplete, setKtxSetupDatabaseConnectionIds } from './context/project/setup-config.js';
import type { KtxTableListEntry } from './context/scan/types.js';
-import type { KtxCliIo } from './cli-runtime.js';
+import { getKtxCliPackageInfo, type KtxCliIo } from './cli-runtime.js';
import {
errorMessage,
flushPrefixedBufferedCommandOutput,
@@ -35,6 +43,10 @@ import {
type PickDatabaseScopeArgs,
} from './database-tree-picker.js';
import { withMultiselectNavigation, withTextInputNavigation } from './prompt-navigation.js';
+import { createKtxCliHistoricSqlRuntime } from './local-adapters.js';
+import type { KtxManagedPythonInstallPolicy } from './managed-python-command.js';
+import type { ManagedPythonCoreDaemonOptions } from './managed-python-http.js';
+import { queryHistoryPullConfig } from './public-ingest.js';
import { runKtxScan } from './scan.js';
import { writeProjectLocalSecretReference } from './setup-secrets.js';
import { isDemoConnection } from './telemetry/demo-detect.js';
@@ -61,6 +73,9 @@ export type KtxSetupDatabaseDriver =
export interface KtxSetupDatabasesArgs {
projectDir: string;
inputMode: 'auto' | 'disabled';
+ yes?: boolean;
+ cliVersion?: string;
+ runtimeInstallPolicy?: KtxManagedPythonInstallPolicy;
databaseDrivers?: KtxSetupDatabaseDriver[];
databaseConnectionIds?: string[];
databaseConnectionId?: string;
@@ -123,6 +138,13 @@ export interface KtxSetupDatabasesDeps {
listTables?: (projectDir: string, connectionId: string, schemas?: string[]) => Promise;
pickDatabaseScope?: (args: PickDatabaseScopeArgs, io: KtxCliIo) => Promise;
historicSqlReadinessProbe?: HistoricSqlReadinessProbe;
+ queryHistoryFilterPicker?: (
+ input: ProposeQueryHistoryServiceAccountFiltersInput,
+ ) => Promise;
+ createQueryHistoryLlmRuntime?: (
+ projectDir: string,
+ project: Awaited>,
+ ) => KtxLlmRuntimePort | null;
}
const DRIVER_OPTIONS: Array<{ value: KtxSetupDatabaseDriver; label: string }> = [
@@ -947,10 +969,14 @@ async function maybeApplyHistoricSqlConfig(input: {
return withQueryHistoryConfig(input.connection, { ...existing, enabled: false });
}
+ const existingFilters =
+ existing.filters && typeof existing.filters === 'object' && !Array.isArray(existing.filters)
+ ? (existing.filters as Record)
+ : {};
const common: Record = {
...existing,
enabled: true,
- filters: historicSqlFiltersForSetup(input.args.queryHistoryServiceAccountPatterns),
+ filters: historicSqlFiltersForSetup(input.args.queryHistoryServiceAccountPatterns, existingFilters),
};
if (dialect === 'postgres') {
@@ -967,9 +993,13 @@ async function maybeApplyHistoricSqlConfig(input: {
});
}
-function historicSqlFiltersForSetup(patterns: string[] | undefined) {
+function historicSqlFiltersForSetup(
+ patterns: string[] | undefined,
+ existingFilters: Record = {},
+) {
const serviceAccountPatterns = patterns ?? [];
return {
+ ...existingFilters,
dropTrivialProbes: true,
...(serviceAccountPatterns.length > 0
? {
@@ -1587,6 +1617,189 @@ async function maybeRunHistoricSqlSetupProbe(input: {
return result.ok;
}
+function hasServiceAccountsBlock(connection: KtxProjectConnectionConfig | undefined): boolean {
+ const queryHistory = queryHistoryConfigRecord(connection);
+ const filters = queryHistory?.filters;
+ if (!filters || typeof filters !== 'object' || Array.isArray(filters)) {
+ return false;
+ }
+ return 'serviceAccounts' in filters;
+}
+
+function printQueryHistoryFilterProposal(io: KtxCliIo, proposal: QueryHistoryFilterProposal): void {
+ if (proposal.excludedRoles.length === 0) {
+ if (proposal.skipped?.reason === 'no-llm') {
+ io.stdout.write('│ Query-history filter picker skipped: no LLM is configured.\n');
+ } else if (proposal.skipped?.reason === 'no-daemon') {
+ io.stdout.write('│ Query-history filter picker skipped: SQL analysis is unavailable.\n');
+ } else if (proposal.skipped?.reason === 'no-in-scope-history') {
+ io.stdout.write('│ Query-history filter picker found no in-scope service-account exclusions.\n');
+ }
+ for (const warning of proposal.warnings) {
+ io.stdout.write(`│ ! ${warning}\n`);
+ }
+ return;
+ }
+
+ io.stdout.write('│ Proposed query-history service-account filters:\n');
+ for (const excluded of proposal.excludedRoles) {
+ io.stdout.write(`│ - ${excluded.role}: ${excluded.reason}\n`);
+ }
+}
+
+async function shouldApplyQueryHistoryFilterProposal(input: {
+ args: KtxSetupDatabasesArgs;
+ prompts: KtxSetupDatabasesPromptAdapter;
+ proposal: QueryHistoryFilterProposal;
+}): Promise {
+ if (input.proposal.excludedRoles.length === 0 || input.proposal.skipped?.reason === 'user-block-present') {
+ return false;
+ }
+ if (input.args.yes === true || input.args.inputMode === 'disabled') {
+ return true;
+ }
+ const choice = await input.prompts.select({
+ message: `Apply ${input.proposal.excludedRoles.length} derived query-history service-account exclusion${
+ input.proposal.excludedRoles.length === 1 ? '' : 's'
+ }?`,
+ options: [
+ { value: 'apply', label: 'Apply derived filters (recommended)' },
+ { value: 'skip', label: 'Leave query history filters unchanged' },
+ ],
+ });
+ return choice === 'apply';
+}
+
+function createSetupQueryHistoryLlmRuntime(input: {
+ projectDir: string;
+ project: Awaited>;
+ deps: KtxSetupDatabasesDeps;
+}): KtxLlmRuntimePort | null {
+ try {
+ return (
+ input.deps.createQueryHistoryLlmRuntime?.(input.projectDir, input.project) ??
+ createLocalKtxLlmRuntimeFromConfig(input.project.config.llm, {
+ projectDir: input.projectDir,
+ })
+ );
+ } catch {
+ return null;
+ }
+}
+
+/** @internal */
+export function managedDaemonOptionsForSetupQueryHistoryPicker(input: {
+ projectDir: string;
+ args: Pick;
+ io: KtxCliIo;
+}): ManagedPythonCoreDaemonOptions {
+ return {
+ cliVersion: input.args.cliVersion ?? getKtxCliPackageInfo().version,
+ projectDir: input.projectDir,
+ installPolicy: input.args.runtimeInstallPolicy ?? (input.args.inputMode === 'disabled' ? 'never' : 'prompt'),
+ io: input.io,
+ };
+}
+
+async function maybeProposeQueryHistoryFilters(input: {
+ projectDir: string;
+ connectionId: string;
+ io: KtxCliIo;
+ deps: KtxSetupDatabasesDeps;
+ args: KtxSetupDatabasesArgs;
+ prompts: KtxSetupDatabasesPromptAdapter;
+}): Promise {
+ const project = await loadKtxProject({ projectDir: input.projectDir });
+ const connection = project.config.connections[input.connectionId];
+ const queryHistory = queryHistoryConfigRecord(connection);
+ if (!connection || queryHistory?.enabled !== true) {
+ return;
+ }
+ const dialect = queryHistoryDialectForConnection(connection);
+ if (!dialect) {
+ return;
+ }
+
+ const picker = input.deps.queryHistoryFilterPicker ?? proposeQueryHistoryServiceAccountFilters;
+ const llmRuntime = createSetupQueryHistoryLlmRuntime({
+ projectDir: input.projectDir,
+ project,
+ deps: input.deps,
+ });
+ if (!llmRuntime && !input.deps.queryHistoryFilterPicker) {
+ printQueryHistoryFilterProposal(input.io, {
+ excludedRoles: [],
+ consideredRoleCount: 0,
+ skipped: { reason: 'no-llm' },
+ warnings: [],
+ });
+ return;
+ }
+
+ const runtime = createKtxCliHistoricSqlRuntime(project, input.connectionId, {
+ managedDaemon: managedDaemonOptionsForSetupQueryHistoryPicker({
+ projectDir: input.projectDir,
+ args: input.args,
+ io: input.io,
+ }),
+ });
+ if (!runtime) {
+ return;
+ }
+ const userServiceAccountsPresent = hasServiceAccountsBlock(connection);
+ const scopeFloor = await resolveQueryHistoryScopeFloor({
+ projectDir: input.projectDir,
+ connectionId: input.connectionId,
+ driver: String(connection.driver ?? ''),
+ connection: connection as Record,
+ storedQueryHistory: queryHistory,
+ });
+ const pullConfig = queryHistoryPullConfig({
+ stored: queryHistory,
+ dialect,
+ enabledTables: scopeFloor.enabledTables,
+ enabledSchemas: scopeFloor.enabledSchemas,
+ modeledTableCatalog: scopeFloor.modeledTableCatalog,
+ scopeFloorWarnings: scopeFloor.warnings,
+ });
+ const proposal = await picker({
+ connectionId: input.connectionId,
+ dialect,
+ queryClient: runtime.queryClient,
+ reader: runtime.reader,
+ sqlAnalysis: runtime.sqlAnalysis,
+ llmRuntime,
+ pullConfig,
+ userServiceAccountsPresent,
+ });
+
+ printQueryHistoryFilterProposal(input.io, proposal);
+ if (proposal.skipped?.reason === 'user-block-present') {
+ input.io.stdout.write('│ Existing query-history service-account filters left unchanged.\n');
+ return;
+ }
+ if (!(await shouldApplyQueryHistoryFilterProposal({ args: input.args, prompts: input.prompts, proposal }))) {
+ return;
+ }
+
+ await writeConnectionConfig({
+ projectDir: input.projectDir,
+ connectionId: input.connectionId,
+ connection: withQueryHistoryConfig(connection, {
+ ...queryHistory,
+ filters: {
+ ...(queryHistory.filters && typeof queryHistory.filters === 'object' && !Array.isArray(queryHistory.filters)
+ ? queryHistory.filters
+ : {}),
+ serviceAccounts: {
+ mode: 'exclude',
+ patterns: proposal.excludedRoles.map((role) => role.pattern),
+ },
+ },
+ }),
+ });
+}
+
async function applyHistoricSqlConfigToExistingConnection(input: {
projectDir: string;
connectionId: string;
@@ -1725,6 +1938,16 @@ async function validateAndScanConnection(input: {
`Schema context complete for ${input.connectionId}`,
[`Changes: ${summarizeScanChanges(scanOutput)}`],
);
+ if (queryHistoryAvailable) {
+ await maybeProposeQueryHistoryFilters({
+ projectDir: input.projectDir,
+ connectionId: input.connectionId,
+ io: input.io,
+ deps: input.deps,
+ args: input.args,
+ prompts: input.prompts,
+ });
+ }
writeSetupSection(input.io, 'Database ready', [
`${input.connectionId} · ${driverDisplay} · schema context complete`,
]);
diff --git a/packages/cli/src/setup.ts b/packages/cli/src/setup.ts
index 7d4fdb0e..a991367e 100644
--- a/packages/cli/src/setup.ts
+++ b/packages/cli/src/setup.ts
@@ -735,6 +735,9 @@ async function runKtxSetupInner(args: KtxSetupArgs, io: KtxCliIo, deps: KtxSetup
{
projectDir: projectResult.projectDir,
inputMode: args.inputMode,
+ yes: args.yes,
+ cliVersion: args.cliVersion,
+ runtimeInstallPolicy: setupRuntimeInstallPolicy(args),
...(args.databaseDrivers ? { databaseDrivers: args.databaseDrivers } : {}),
...(args.databaseConnectionIds ? { databaseConnectionIds: args.databaseConnectionIds } : {}),
...(args.databaseConnectionId ? { databaseConnectionId: args.databaseConnectionId } : {}),
diff --git a/packages/cli/test/context/ingest/adapters/historic-sql/bigquery-query-history-reader.test.ts b/packages/cli/test/context/ingest/adapters/historic-sql/bigquery-query-history-reader.test.ts
index 74393e4b..e3222ab5 100644
--- a/packages/cli/test/context/ingest/adapters/historic-sql/bigquery-query-history-reader.test.ts
+++ b/packages/cli/test/context/ingest/adapters/historic-sql/bigquery-query-history-reader.test.ts
@@ -91,7 +91,10 @@ describe('BigQueryHistoricSqlQueryHistoryReader', () => {
40,
0.05,
null,
- JSON.stringify([{ user: 'analyst@example.test', executions: 1 }]),
+ JSON.stringify([
+ { user: 'svc-loader@example.test', executions: 40 },
+ { user: 'analyst@example.test', executions: 2 },
+ ]),
],
],
totalRows: 1,
@@ -103,15 +106,25 @@ describe('BigQueryHistoricSqlQueryHistoryReader', () => {
for await (const row of reader.fetchAggregated(
client,
{ start: new Date('2026-02-10T00:00:00.000Z'), end: new Date('2026-05-11T00:00:00.000Z') },
- { dialect: 'bigquery', minExecutions: 5, windowDays: 90, enabledTables: [], filters: { dropTrivialProbes: true }, redactionPatterns: [], staleArchiveAfterDays: 90 },
+ { dialect: 'bigquery', minExecutions: 5, windowDays: 90, enabledTables: [], enabledSchemas: [], modeledTableCatalog: [], scopeFloorWarnings: [], filters: { dropTrivialProbes: true }, redactionPatterns: [], staleArchiveAfterDays: 90 },
)) {
rows.push(row);
}
const sql = firstQuery(client);
+ expect(sql).toContain('WITH filtered_jobs AS');
+ expect(sql).toContain('query_info.query_hashes.normalized_literals');
+ expect(sql).toContain('TO_HEX(SHA256(query))');
+ expect(sql).toContain('AS template_id');
+ expect(sql).toContain('template_stats AS');
+ expect(sql).toContain('template_users AS');
expect(sql).toContain('COUNT(*) AS executions');
expect(sql).toContain('COUNT(DISTINCT user_email) AS distinct_users');
- expect(sql).toContain('GROUP BY query_hash');
+ expect(sql).toContain('GROUP BY template_id');
+ expect(sql).toContain('GROUP BY template_id, user_email');
+ expect(sql).toContain('ORDER BY users.executions DESC');
+ expect(sql).not.toMatch(/\bquery_hash\b/);
+ expect(sql).not.toContain('LIMIT 5');
expect(sql).toContain('HAVING COUNT(*) >= 5');
expect(rows).toMatchObject([
{
@@ -120,7 +133,10 @@ describe('BigQueryHistoricSqlQueryHistoryReader', () => {
executions: 42,
errorRate: 0.05,
},
- topUsers: [{ user: 'analyst@example.test', executions: 1 }],
+ topUsers: [
+ { user: 'svc-loader@example.test', executions: 40 },
+ { user: 'analyst@example.test', executions: 2 },
+ ],
},
]);
});
@@ -137,6 +153,9 @@ describe('BigQueryHistoricSqlQueryHistoryReader', () => {
minExecutions: 5,
windowDays: 90,
enabledTables: [],
+ enabledSchemas: [],
+ modeledTableCatalog: [],
+ scopeFloorWarnings: [],
filters: { dropTrivialProbes: true },
redactionPatterns: [],
staleArchiveAfterDays: 90,
diff --git a/packages/cli/test/context/ingest/adapters/historic-sql/chunk-unified.test.ts b/packages/cli/test/context/ingest/adapters/historic-sql/chunk-unified.test.ts
index a8e99c39..f3d7d293 100644
--- a/packages/cli/test/context/ingest/adapters/historic-sql/chunk-unified.test.ts
+++ b/packages/cli/test/context/ingest/adapters/historic-sql/chunk-unified.test.ts
@@ -30,6 +30,7 @@ async function writeUnifiedStagedDir(root: string): Promise {
});
await writeJson(root, 'tables/public.orders.json', {
table: 'public.orders',
+ tableRef: { catalog: null, db: 'public', name: 'orders' },
stats: {
executionsBucket: '10-100',
distinctUsersBucket: '2-5',
@@ -46,7 +47,10 @@ async function writeUnifiedStagedDir(root: string): Promise {
{
id: 'orders',
canonicalSql: 'select * from public.orders join public.customers on true',
- tablesTouched: ['public.orders', 'public.customers'],
+ tablesTouched: [
+ { catalog: null, db: 'public', name: 'orders' },
+ { catalog: null, db: 'public', name: 'customers' },
+ ],
executionsBucket: '10-100',
distinctUsersBucket: '2-5',
dialect: 'postgres',
@@ -58,7 +62,10 @@ async function writeUnifiedStagedDir(root: string): Promise {
{
id: 'orders',
canonicalSql: 'select * from public.orders join public.customers on true',
- tablesTouched: ['public.orders', 'public.customers'],
+ tablesTouched: [
+ { catalog: null, db: 'public', name: 'orders' },
+ { catalog: null, db: 'public', name: 'customers' },
+ ],
executionsBucket: '10-100',
distinctUsersBucket: '2-5',
dialect: 'postgres',
@@ -155,7 +162,10 @@ describe('chunkHistoricSqlUnifiedStagedDir', () => {
{
id: 'line-items',
canonicalSql: 'select * from public.orders join public.line_items on true',
- tablesTouched: ['public.orders', 'public.line_items'],
+ tablesTouched: [
+ { catalog: null, db: 'public', name: 'orders' },
+ { catalog: null, db: 'public', name: 'line_items' },
+ ],
executionsBucket: '10-100',
distinctUsersBucket: '2-5',
dialect: 'postgres',
diff --git a/packages/cli/test/context/ingest/adapters/historic-sql/historic-sql.adapter.test.ts b/packages/cli/test/context/ingest/adapters/historic-sql/historic-sql.adapter.test.ts
index 850be576..dcd00d32 100644
--- a/packages/cli/test/context/ingest/adapters/historic-sql/historic-sql.adapter.test.ts
+++ b/packages/cli/test/context/ingest/adapters/historic-sql/historic-sql.adapter.test.ts
@@ -76,7 +76,10 @@ describe('HistoricSqlSourceAdapter', () => {
[
'pg:1',
{
- tablesTouched: ['public.orders', 'public.customers'],
+ tablesTouched: [
+ { catalog: null, db: 'public', name: 'orders' },
+ { catalog: null, db: 'public', name: 'customers' },
+ ],
columnsByClause: { select: ['status'], join: ['customer_id', 'id'], groupBy: ['status'] },
},
],
diff --git a/packages/cli/test/context/ingest/adapters/historic-sql/local-ingest-acceptance.test.ts b/packages/cli/test/context/ingest/adapters/historic-sql/local-ingest-acceptance.test.ts
index 48e5744b..e818f1c9 100644
--- a/packages/cli/test/context/ingest/adapters/historic-sql/local-ingest-acceptance.test.ts
+++ b/packages/cli/test/context/ingest/adapters/historic-sql/local-ingest-acceptance.test.ts
@@ -126,7 +126,10 @@ function acceptanceSqlAnalysis(): SqlAnalysisPort {
items.map((item) => [
item.id,
{
- tablesTouched: ['public.orders', 'public.customers'],
+ tablesTouched: [
+ { catalog: null, db: 'public', name: 'orders' },
+ { catalog: null, db: 'public', name: 'customers' },
+ ],
columnsByClause: {
select: ['status', 'segment'],
where: ['status'],
diff --git a/packages/cli/test/context/ingest/adapters/historic-sql/pattern-inputs.test.ts b/packages/cli/test/context/ingest/adapters/historic-sql/pattern-inputs.test.ts
index 9fae5e08..780fcf3d 100644
--- a/packages/cli/test/context/ingest/adapters/historic-sql/pattern-inputs.test.ts
+++ b/packages/cli/test/context/ingest/adapters/historic-sql/pattern-inputs.test.ts
@@ -9,11 +9,18 @@ import type { StagedPatternsInput } from '../../../../../src/context/ingest/adap
type PatternTemplate = StagedPatternsInput['templates'][number];
+function tableRef(value: string): { catalog: string | null; db: string | null; name: string } {
+ const parts = value.split('.');
+ if (parts.length === 3) return { catalog: parts[0]!, db: parts[1]!, name: parts[2]! };
+ if (parts.length === 2) return { catalog: null, db: parts[0]!, name: parts[1]! };
+ return { catalog: null, db: null, name: value };
+}
+
function template(id: string, tablesTouched: string[], canonicalSql = 'select 1'): PatternTemplate {
return {
id,
canonicalSql,
- tablesTouched,
+ tablesTouched: tablesTouched.map(tableRef),
executionsBucket: '10-100',
distinctUsersBucket: '2-5',
dialect: 'postgres',
@@ -32,7 +39,7 @@ describe('historic-SQL pattern input sharding', () => {
],
};
- const result = splitHistoricSqlPatternInputs(input, { maxBytes: 760 });
+ const result = splitHistoricSqlPatternInputs(input, { maxBytes: 1200 });
expect(result.auditInput.templates.map((entry) => entry.id)).toEqual([
'orders-customers-1',
@@ -51,7 +58,7 @@ describe('historic-SQL pattern input sharding', () => {
'orders-customers-1',
'orders-customers-2',
]);
- expect(result.shards.every((shard) => shard.byteLength <= 760)).toBe(true);
+ expect(result.shards.every((shard) => shard.byteLength <= 1200)).toBe(true);
expect(result.shards.flatMap((shard) => shard.input.templates).some((entry) => entry.id === 'single-table-orders')).toBe(false);
expect(result.warnings).toEqual([]);
});
diff --git a/packages/cli/test/context/ingest/adapters/historic-sql/postgres-pgss-reader.test.ts b/packages/cli/test/context/ingest/adapters/historic-sql/postgres-pgss-reader.test.ts
index 41baf331..4c9fc7bb 100644
--- a/packages/cli/test/context/ingest/adapters/historic-sql/postgres-pgss-reader.test.ts
+++ b/packages/cli/test/context/ingest/adapters/historic-sql/postgres-pgss-reader.test.ts
@@ -215,7 +215,7 @@ describe('PostgresPgssReader aggregate path', () => {
for await (const row of reader.fetchAggregated(
{ executeQuery },
{ start: new Date('2026-02-10T00:00:00.000Z'), end: new Date('2026-05-11T00:00:00.000Z') },
- { dialect: 'postgres', minExecutions: 5, enabledTables: [], filters: { dropTrivialProbes: true }, redactionPatterns: [], staleArchiveAfterDays: 90 },
+ { dialect: 'postgres', minExecutions: 5, enabledTables: [], enabledSchemas: [], modeledTableCatalog: [], scopeFloorWarnings: [], filters: { dropTrivialProbes: true }, redactionPatterns: [], staleArchiveAfterDays: 90 },
)) {
rows.push(row);
}
diff --git a/packages/cli/test/context/ingest/adapters/historic-sql/query-history-filter-picker.test.ts b/packages/cli/test/context/ingest/adapters/historic-sql/query-history-filter-picker.test.ts
new file mode 100644
index 00000000..4c295092
--- /dev/null
+++ b/packages/cli/test/context/ingest/adapters/historic-sql/query-history-filter-picker.test.ts
@@ -0,0 +1,274 @@
+import { describe, expect, it, vi } from 'vitest';
+import type { KtxLlmRuntimePort } from '../../../../../src/context/llm/runtime-port.js';
+import type {
+ SqlAnalysisBatchItem,
+ SqlAnalysisBatchResult,
+ SqlAnalysisPort,
+} from '../../../../../src/context/sql-analysis/ports.js';
+import {
+ proposeQueryHistoryServiceAccountFilters,
+ regexEscapeForExactRolePattern,
+} from '../../../../../src/context/ingest/adapters/historic-sql/query-history-filter-picker.js';
+import type {
+ AggregatedTemplate,
+ HistoricSqlReader,
+} from '../../../../../src/context/ingest/adapters/historic-sql/types.js';
+
+function aggregate(overrides: Partial & { templateId: string; canonicalSql: string }): AggregatedTemplate {
+ return {
+ templateId: overrides.templateId,
+ canonicalSql: overrides.canonicalSql,
+ dialect: overrides.dialect ?? 'postgres',
+ stats: overrides.stats ?? {
+ executions: 25,
+ distinctUsers: 1,
+ firstSeen: '2026-05-01T00:00:00.000Z',
+ lastSeen: '2026-06-01T00:00:00.000Z',
+ p50RuntimeMs: 50,
+ p95RuntimeMs: 100,
+ errorRate: 0,
+ rowsProduced: 10,
+ },
+ topUsers: overrides.topUsers ?? [{ user: 'analyst', executions: 25 }],
+ };
+}
+
+function reader(...templates: AggregatedTemplate[]): HistoricSqlReader {
+ return {
+ async probe() {
+ return { warnings: [], info: [] };
+ },
+ async *fetchAggregated() {
+ for (const template of templates) {
+ yield template;
+ }
+ },
+ };
+}
+
+function sqlAnalysis(tablesById: Record>): SqlAnalysisPort {
+ return {
+ analyzeForFingerprint: vi.fn(),
+ analyzeBatch: vi.fn(async (items: SqlAnalysisBatchItem[]): Promise> =>
+ new Map(
+ items.map((item) => [
+ item.id,
+ {
+ tablesTouched: tablesById[item.id] ?? [],
+ columnsByClause: {},
+ },
+ ]),
+ ),
+ ),
+ validateReadOnly: vi.fn(async () => ({ ok: true })),
+ };
+}
+
+function llm(decisions: Array<{ role: string; exclude: boolean; reason: string }>): KtxLlmRuntimePort {
+ const generateObject = vi.fn(async () => ({ roles: decisions })) as KtxLlmRuntimePort['generateObject'];
+ return {
+ generateText: vi.fn(),
+ generateObject,
+ runAgentLoop: vi.fn(),
+ };
+}
+
+describe('query-history filter picker', () => {
+ it('emits anchored escaped patterns for excluded roles from one batched LLM call', async () => {
+ const runtime = llm([
+ { role: 'svc.loader+prod', exclude: true, reason: 'Runs recurring loader traffic only.' },
+ { role: 'analyst', exclude: false, reason: 'Interactive analytic usage.' },
+ ]);
+ const analysis = sqlAnalysis({
+ loader: [{ catalog: null, db: 'analytics', name: 'orders' }],
+ analyst: [{ catalog: null, db: 'analytics', name: 'orders' }],
+ });
+
+ const proposal = await proposeQueryHistoryServiceAccountFilters({
+ connectionId: 'warehouse',
+ dialect: 'postgres',
+ queryClient: {},
+ reader: reader(
+ aggregate({
+ templateId: 'loader',
+ canonicalSql: 'merge into analytics.orders using staging.orders_delta on orders.id = orders_delta.id',
+ topUsers: [{ user: 'svc.loader+prod', executions: 40 }],
+ }),
+ aggregate({
+ templateId: 'analyst',
+ canonicalSql: 'select status, count(*) from analytics.orders group by status',
+ topUsers: [{ user: 'analyst', executions: 25 }],
+ }),
+ ),
+ sqlAnalysis: analysis,
+ llmRuntime: runtime,
+ pullConfig: {
+ dialect: 'postgres',
+ enabledSchemas: ['analytics'],
+ enabledTables: [],
+ modeledTableCatalog: [{ catalog: null, db: 'analytics', name: 'orders' }],
+ filters: { dropTrivialProbes: true },
+ },
+ now: new Date('2026-06-03T00:00:00.000Z'),
+ });
+
+ expect(runtime.generateObject).toHaveBeenCalledTimes(1);
+ expect(proposal).toMatchObject({
+ excludedRoles: [
+ {
+ role: 'svc.loader+prod',
+ pattern: '^svc\\.loader\\+prod$',
+ reason: 'Runs recurring loader traffic only.',
+ },
+ ],
+ consideredRoleCount: 2,
+ skipped: null,
+ warnings: [],
+ });
+ });
+
+ it('redacts representative SQL before sending role records to the LLM', async () => {
+ const originalSql =
+ "select * from public.api_events where api_key = 'sk_live_abc123' and note = 'Secret_Token_9f'"; // pragma: allowlist secret
+ const runtime = llm([
+ { role: 'svc_loader', exclude: false, reason: 'Keep by default.' },
+ { role: 'analyst', exclude: false, reason: 'Interactive analytic usage.' },
+ ]);
+ const analysis = sqlAnalysis({
+ secret: [{ catalog: null, db: 'public', name: 'api_events' }],
+ analyst: [{ catalog: null, db: 'public', name: 'orders' }],
+ });
+
+ await proposeQueryHistoryServiceAccountFilters({
+ connectionId: 'warehouse',
+ dialect: 'postgres',
+ queryClient: {},
+ reader: reader(
+ aggregate({
+ templateId: 'secret',
+ canonicalSql: originalSql,
+ topUsers: [{ user: 'svc_loader', executions: 30 }],
+ }),
+ aggregate({
+ templateId: 'analyst',
+ canonicalSql: 'select status, count(*) from public.orders group by status',
+ topUsers: [{ user: 'analyst', executions: 25 }],
+ }),
+ ),
+ sqlAnalysis: analysis,
+ llmRuntime: runtime,
+ pullConfig: {
+ dialect: 'postgres',
+ enabledSchemas: ['public'],
+ enabledTables: [],
+ modeledTableCatalog: [],
+ redactionPatterns: ['sk_live_[A-Za-z0-9]+', '(?i)secret_token_[a-z0-9]+'],
+ filters: { dropTrivialProbes: true },
+ },
+ now: new Date('2026-06-03T00:00:00.000Z'),
+ });
+
+ expect(analysis.analyzeBatch).toHaveBeenCalledWith(
+ [
+ { id: 'secret', sql: originalSql },
+ { id: 'analyst', sql: 'select status, count(*) from public.orders group by status' },
+ ],
+ 'postgres',
+ undefined,
+ );
+ const call = vi.mocked(runtime.generateObject).mock.calls[0]?.[0];
+ expect(call?.prompt).toContain('[REDACTED]');
+ expect(call?.prompt).not.toContain('sk_live_abc123');
+ expect(call?.prompt).not.toContain('Secret_Token_9f');
+ });
+
+ it('fails open with no LLM runtime', async () => {
+ const proposal = await proposeQueryHistoryServiceAccountFilters({
+ connectionId: 'warehouse',
+ dialect: 'postgres',
+ queryClient: {},
+ reader: reader(),
+ sqlAnalysis: sqlAnalysis({}),
+ llmRuntime: null,
+ pullConfig: { dialect: 'postgres', filters: { dropTrivialProbes: true } },
+ });
+
+ expect(proposal).toEqual({
+ excludedRoles: [],
+ consideredRoleCount: 0,
+ skipped: { reason: 'no-llm' },
+ warnings: [],
+ });
+ });
+
+ it('proposes nothing for a single-role stack', async () => {
+ const runtime = llm([{ role: 'warehouse_user', exclude: true, reason: 'Only observed role.' }]);
+
+ const proposal = await proposeQueryHistoryServiceAccountFilters({
+ connectionId: 'warehouse',
+ dialect: 'postgres',
+ queryClient: {},
+ reader: reader(
+ aggregate({
+ templateId: 'single-role',
+ canonicalSql: 'select * from analytics.orders',
+ topUsers: [{ user: 'warehouse_user', executions: 40 }],
+ }),
+ ),
+ sqlAnalysis: sqlAnalysis({
+ 'single-role': [{ catalog: null, db: 'analytics', name: 'orders' }],
+ }),
+ llmRuntime: runtime,
+ pullConfig: { dialect: 'postgres', enabledSchemas: ['analytics'], filters: { dropTrivialProbes: true } },
+ });
+
+ expect(runtime.generateObject).not.toHaveBeenCalled();
+ expect(proposal.excludedRoles).toEqual([]);
+ expect(proposal.skipped).toEqual({ reason: 'no-in-scope-history' });
+ });
+
+ it('keeps clean in-scope history when the model excludes nothing', async () => {
+ const proposal = await proposeQueryHistoryServiceAccountFilters({
+ connectionId: 'warehouse',
+ dialect: 'bigquery',
+ queryClient: {},
+ reader: reader(
+ aggregate({
+ templateId: 'dashboard',
+ canonicalSql: 'select status, count(*) from `demo.analytics.orders` group by status',
+ dialect: 'bigquery',
+ topUsers: [{ user: 'bi_runner', executions: 1 }],
+ }),
+ aggregate({
+ templateId: 'analyst',
+ canonicalSql: 'select * from `demo.analytics.orders` where id = @id',
+ dialect: 'bigquery',
+ topUsers: [{ user: 'analyst', executions: 1 }],
+ }),
+ ),
+ sqlAnalysis: sqlAnalysis({
+ dashboard: [{ catalog: 'demo', db: 'analytics', name: 'orders' }],
+ analyst: [{ catalog: 'demo', db: 'analytics', name: 'orders' }],
+ }),
+ llmRuntime: llm([
+ { role: 'bi_runner', exclude: false, reason: 'Dashboard usage is analytic.' },
+ { role: 'analyst', exclude: false, reason: 'Interactive analyst usage.' },
+ ]),
+ pullConfig: {
+ dialect: 'bigquery',
+ windowDays: 90,
+ enabledSchemas: ['analytics'],
+ filters: { dropTrivialProbes: true },
+ },
+ });
+
+ expect(proposal.excludedRoles).toEqual([]);
+ expect(proposal.consideredRoleCount).toBe(2);
+ expect(proposal.skipped).toBeNull();
+ });
+
+ it('escapes regex metacharacters for exact role matches', () => {
+ expect(regexEscapeForExactRolePattern('svc.loader+prod')).toBe('^svc\\.loader\\+prod$');
+ expect(regexEscapeForExactRolePattern('team[etl](west)')).toBe('^team\\[etl\\]\\(west\\)$');
+ });
+});
diff --git a/packages/cli/test/context/ingest/adapters/historic-sql/scope-floor.test.ts b/packages/cli/test/context/ingest/adapters/historic-sql/scope-floor.test.ts
new file mode 100644
index 00000000..597cae46
--- /dev/null
+++ b/packages/cli/test/context/ingest/adapters/historic-sql/scope-floor.test.ts
@@ -0,0 +1,194 @@
+import { mkdir, mkdtemp, writeFile } from 'node:fs/promises';
+import { tmpdir } from 'node:os';
+import { join } from 'node:path';
+import { describe, expect, it } from 'vitest';
+import { resolveQueryHistoryScopeFloor } from '../../../../../src/context/ingest/adapters/historic-sql/scope-floor.js';
+
+async function tempProject(): Promise {
+ return mkdtemp(join(tmpdir(), 'ktx-qh-scope-'));
+}
+
+async function seedLiveScanTable(
+ projectDir: string,
+ connectionId: string,
+ syncId: string,
+ table: { catalog: string | null; db: string | null; name: string },
+): Promise {
+ const root = join(projectDir, 'raw-sources', connectionId, 'live-database', syncId);
+ await mkdir(join(root, 'tables'), { recursive: true });
+ await writeFile(
+ join(root, 'connection.json'),
+ `${JSON.stringify({ connectionId, driver: 'postgres' }, null, 2)}\n`,
+ 'utf-8',
+ );
+ await writeFile(
+ join(root, 'tables', `${table.db ?? 'default'}-${table.name}.json`),
+ `${JSON.stringify(
+ {
+ ...table,
+ kind: 'table',
+ comment: null,
+ estimatedRows: null,
+ columns: [],
+ foreignKeys: [],
+ },
+ null,
+ 2,
+ )}\n`,
+ 'utf-8',
+ );
+ await writeFile(
+ join(root, 'scan-report.json'),
+ `${JSON.stringify(
+ {
+ connectionId,
+ driver: 'postgres',
+ syncId,
+ runId: `scan-${syncId}`,
+ trigger: 'cli',
+ mode: 'enriched',
+ dryRun: false,
+ artifactPaths: {
+ rawSourcesDir: `raw-sources/${connectionId}/live-database/${syncId}`,
+ reportPath: `raw-sources/${connectionId}/live-database/${syncId}/scan-report.json`,
+ manifestShards: [],
+ enrichmentArtifacts: [],
+ },
+ counts: {},
+ warnings: [],
+ enrichment: {},
+ enrichmentState: {},
+ },
+ null,
+ 2,
+ )}\n`,
+ 'utf-8',
+ );
+}
+
+describe('resolveQueryHistoryScopeFloor', () => {
+ it('computes modeled schemas from connection schemas plus semantic source tables', async () => {
+ const projectDir = await tempProject();
+ await mkdir(join(projectDir, 'semantic-layer/warehouse'), { recursive: true });
+ await writeFile(
+ join(projectDir, 'semantic-layer/warehouse/revenue.yaml'),
+ [
+ 'name: revenue',
+ 'table: orbit_analytics.mart_revenue',
+ 'grain: [id]',
+ 'columns:',
+ ' - name: id',
+ ' type: string',
+ '',
+ ].join('\n'),
+ 'utf-8',
+ );
+ await seedLiveScanTable(projectDir, 'warehouse', 'sync-1', {
+ catalog: null,
+ db: 'orbit_raw',
+ name: 'accounts',
+ });
+
+ const scope = await resolveQueryHistoryScopeFloor({
+ projectDir,
+ connectionId: 'warehouse',
+ driver: 'postgres',
+ connection: { driver: 'postgres', schemas: ['orbit_raw'] },
+ storedQueryHistory: {},
+ });
+
+ expect(scope.enabledSchemas).toEqual(['orbit_analytics', 'orbit_raw']);
+ expect(scope.modeledTableCatalog).toEqual([
+ { catalog: null, db: 'orbit_analytics', name: 'mart_revenue' },
+ { catalog: null, db: 'orbit_raw', name: 'accounts' },
+ ]);
+ expect(scope.enabledTables).toEqual([]);
+ expect(scope.floorDisabled).toBe(false);
+ });
+
+ it('uses explicit enabledTables before explicit enabledSchemas and computed scope', async () => {
+ const scope = await resolveQueryHistoryScopeFloor({
+ projectDir: await tempProject(),
+ connectionId: 'warehouse',
+ driver: 'postgres',
+ connection: { driver: 'postgres', schemas: ['orbit_raw'] },
+ storedQueryHistory: {
+ enabledTables: ['orbit_analytics.mart_revenue'],
+ enabledSchemas: ['orbit_raw'],
+ },
+ });
+
+ expect(scope.enabledTables).toEqual([{ catalog: null, db: 'orbit_analytics', name: 'mart_revenue' }]);
+ expect(scope.enabledSchemas).toEqual([]);
+ expect(scope.floorDisabled).toBe(false);
+ });
+
+ it('disables the floor for enabledSchemas star', async () => {
+ const scope = await resolveQueryHistoryScopeFloor({
+ projectDir: await tempProject(),
+ connectionId: 'warehouse',
+ driver: 'postgres',
+ connection: { driver: 'postgres', schemas: ['orbit_raw'] },
+ storedQueryHistory: { enabledSchemas: ['*'] },
+ });
+
+ expect(scope.enabledTables).toEqual([]);
+ expect(scope.enabledSchemas).toEqual(['*']);
+ expect(scope.floorDisabled).toBe(true);
+ });
+
+ it('adds latest live-database scan tables to the modeled table catalog', async () => {
+ const projectDir = await tempProject();
+ await mkdir(join(projectDir, 'semantic-layer/warehouse'), { recursive: true });
+ await writeFile(
+ join(projectDir, 'semantic-layer/warehouse/revenue.yaml'),
+ [
+ 'name: revenue',
+ 'table: orbit_analytics.mart_revenue',
+ 'grain: [id]',
+ 'columns:',
+ ' - name: id',
+ ' type: string',
+ '',
+ ].join('\n'),
+ 'utf-8',
+ );
+ await seedLiveScanTable(projectDir, 'warehouse', 'sync-1', {
+ catalog: null,
+ db: 'orbit_raw',
+ name: 'accounts',
+ });
+
+ const scope = await resolveQueryHistoryScopeFloor({
+ projectDir,
+ connectionId: 'warehouse',
+ driver: 'postgres',
+ connection: { driver: 'postgres', schemas: ['orbit_raw'] },
+ storedQueryHistory: {},
+ });
+
+ expect(scope.enabledSchemas).toEqual(['orbit_analytics', 'orbit_raw']);
+ expect(scope.modeledTableCatalog).toEqual([
+ { catalog: null, db: 'orbit_analytics', name: 'mart_revenue' },
+ { catalog: null, db: 'orbit_raw', name: 'accounts' },
+ ]);
+ expect(scope.warnings).toEqual([]);
+ expect(scope.floorDisabled).toBe(false);
+ });
+
+ it('fails open when schema scope exists but the scan catalog is unavailable', async () => {
+ const scope = await resolveQueryHistoryScopeFloor({
+ projectDir: await tempProject(),
+ connectionId: 'warehouse',
+ driver: 'postgres',
+ connection: { driver: 'postgres', schemas: ['orbit_raw'] },
+ storedQueryHistory: {},
+ });
+
+ expect(scope.enabledTables).toEqual([]);
+ expect(scope.enabledSchemas).toEqual(['*']);
+ expect(scope.modeledTableCatalog).toEqual([]);
+ expect(scope.floorDisabled).toBe(true);
+ expect(scope.warnings).toContain('query_history_scope_floor_disabled:catalog_unavailable');
+ });
+});
diff --git a/packages/cli/test/context/ingest/adapters/historic-sql/scope-membership.test.ts b/packages/cli/test/context/ingest/adapters/historic-sql/scope-membership.test.ts
new file mode 100644
index 00000000..bfcefc22
--- /dev/null
+++ b/packages/cli/test/context/ingest/adapters/historic-sql/scope-membership.test.ts
@@ -0,0 +1,51 @@
+import { describe, expect, it } from 'vitest';
+import {
+ includedQueryHistoryTableRefs,
+ isQueryHistoryScopeFloorDisabled,
+ shouldFailOpenQueryHistoryScope,
+} from '../../../../../src/context/ingest/adapters/historic-sql/scope-membership.js';
+import type { KtxTableRef } from '../../../../../src/context/scan/types.js';
+
+function ref(db: string | null, name: string, catalog: string | null = null): KtxTableRef {
+ return { catalog, db, name };
+}
+
+describe('query-history scope membership', () => {
+ it('prefers explicit enabled tables over schema scope', () => {
+ const orders = ref('analytics', 'orders');
+ const noise = ref('metabase', 'application_table');
+
+ expect(
+ includedQueryHistoryTableRefs([orders, noise], {
+ enabledTables: [orders],
+ enabledSchemas: ['metabase'],
+ }),
+ ).toEqual([orders]);
+ });
+
+ it('matches schema scope by the db component across catalogs', () => {
+ const modeled = ref('orbit_analytics', 'orders', 'demo-project');
+ const noise = ref('metabase', 'application_table', 'demo-project');
+
+ expect(
+ includedQueryHistoryTableRefs([modeled, noise], {
+ enabledTables: [],
+ enabledSchemas: ['orbit_analytics'],
+ }),
+ ).toEqual([modeled]);
+ });
+
+ it('keeps every touched ref when wildcard scope disables the floor', () => {
+ const tables = [ref('analytics', 'orders'), ref('metabase', 'application_table')];
+
+ expect(isQueryHistoryScopeFloorDisabled({ enabledTables: [], enabledSchemas: ['*'] })).toBe(true);
+ expect(includedQueryHistoryTableRefs(tables, { enabledTables: [], enabledSchemas: ['*'] })).toEqual(tables);
+ });
+
+ it('fails open when no tables, schemas, or wildcard are configured', () => {
+ const tables = [ref('metabase', 'application_table')];
+
+ expect(shouldFailOpenQueryHistoryScope({ enabledTables: [], enabledSchemas: [] })).toBe(true);
+ expect(includedQueryHistoryTableRefs(tables, { enabledTables: [], enabledSchemas: [] })).toEqual(tables);
+ });
+});
diff --git a/packages/cli/test/context/ingest/adapters/historic-sql/snowflake-query-history-reader.test.ts b/packages/cli/test/context/ingest/adapters/historic-sql/snowflake-query-history-reader.test.ts
index 7307fcdd..ab76c533 100644
--- a/packages/cli/test/context/ingest/adapters/historic-sql/snowflake-query-history-reader.test.ts
+++ b/packages/cli/test/context/ingest/adapters/historic-sql/snowflake-query-history-reader.test.ts
@@ -90,7 +90,10 @@ describe('SnowflakeHistoricSqlQueryHistoryReader', () => {
40,
0.05,
100,
- JSON.stringify([{ user: 'ANALYST', executions: 1 }]),
+ JSON.stringify([
+ { user: 'SVC_LOADER', executions: 40 },
+ { user: 'ANALYST', executions: 2 },
+ ]),
],
],
totalRows: 1,
@@ -102,15 +105,20 @@ describe('SnowflakeHistoricSqlQueryHistoryReader', () => {
for await (const row of reader.fetchAggregated(
client,
{ start: new Date('2026-02-10T00:00:00.000Z'), end: new Date('2026-05-11T00:00:00.000Z') },
- { dialect: 'snowflake', minExecutions: 5, windowDays: 90, enabledTables: [], filters: { dropTrivialProbes: true }, redactionPatterns: [], staleArchiveAfterDays: 90 },
+ { dialect: 'snowflake', minExecutions: 5, windowDays: 90, enabledTables: [], enabledSchemas: [], modeledTableCatalog: [], scopeFloorWarnings: [], filters: { dropTrivialProbes: true }, redactionPatterns: [], staleArchiveAfterDays: 90 },
)) {
rows.push(row);
}
const sql = firstQuery(client);
+ expect(sql).toContain('WITH filtered_queries AS');
+ expect(sql).toContain('template_stats AS');
+ expect(sql).toContain('template_users AS');
expect(sql).toContain('SNOWFLAKE.ACCOUNT_USAGE.QUERY_HISTORY');
expect(sql).toContain('COUNT(*) AS executions');
- expect(sql).toContain('GROUP BY query_hash');
+ expect(sql).toContain('COUNT(DISTINCT user_name) AS distinct_users');
+ expect(sql).toContain('GROUP BY query_hash, user_name');
+ expect(sql).toContain('ORDER BY users.executions DESC');
expect(sql).toContain('HAVING COUNT(*) >= 5');
expect(rows).toMatchObject([
{
@@ -119,7 +127,10 @@ describe('SnowflakeHistoricSqlQueryHistoryReader', () => {
executions: 42,
errorRate: 0.05,
},
- topUsers: [{ user: 'ANALYST', executions: 1 }],
+ topUsers: [
+ { user: 'SVC_LOADER', executions: 40 },
+ { user: 'ANALYST', executions: 2 },
+ ],
},
]);
});
@@ -136,6 +147,9 @@ describe('SnowflakeHistoricSqlQueryHistoryReader', () => {
minExecutions: 5,
windowDays: 90,
enabledTables: [],
+ enabledSchemas: [],
+ modeledTableCatalog: [],
+ scopeFloorWarnings: [],
filters: { dropTrivialProbes: true },
redactionPatterns: [],
staleArchiveAfterDays: 90,
diff --git a/packages/cli/test/context/ingest/adapters/historic-sql/stage-unified.test.ts b/packages/cli/test/context/ingest/adapters/historic-sql/stage-unified.test.ts
index 630a3939..a104508d 100644
--- a/packages/cli/test/context/ingest/adapters/historic-sql/stage-unified.test.ts
+++ b/packages/cli/test/context/ingest/adapters/historic-sql/stage-unified.test.ts
@@ -14,6 +14,13 @@ async function readJson(root: string, relPath: string): Promise {
return JSON.parse(await readFile(join(root, relPath), 'utf-8')) as T;
}
+function tableRef(value: string): { catalog: string | null; db: string | null; name: string } {
+ const parts = value.split('.');
+ if (parts.length === 3) return { catalog: parts[0]!, db: parts[1]!, name: parts[2]! };
+ if (parts.length === 2) return { catalog: null, db: parts[0]!, name: parts[1]! };
+ return { catalog: null, db: null, name: value };
+}
+
function aggregate(overrides: Partial & { templateId: string; canonicalSql: string }): AggregatedTemplate {
return {
templateId: overrides.templateId,
@@ -72,7 +79,7 @@ describe('stageHistoricSqlAggregatedSnapshot', () => {
[
'orders-by-status',
{
- tablesTouched: ['public.orders', 'public.customers'],
+ tablesTouched: [tableRef('public.orders'), tableRef('public.customers')],
columnsByClause: {
select: ['status'],
where: ['created_at'],
@@ -94,6 +101,7 @@ describe('stageHistoricSqlAggregatedSnapshot', () => {
sqlAnalysis,
pullConfig: {
dialect: 'postgres',
+ enabledSchemas: ['public'],
filters: {
serviceAccounts: { patterns: ['^svc_'], mode: 'exclude' },
},
@@ -111,6 +119,7 @@ describe('stageHistoricSqlAggregatedSnapshot', () => {
{ id: 'bad-parse', sql: 'select broken from' },
],
'postgres',
+ undefined,
);
expect(await readdir(join(stagedDir, 'tables'))).toEqual(['public.customers.json', 'public.orders.json']);
@@ -131,6 +140,7 @@ describe('stageHistoricSqlAggregatedSnapshot', () => {
const orders = await readJson>(stagedDir, 'tables/public.orders.json');
expect(orders).toMatchObject({
table: 'public.orders',
+ tableRef: tableRef('public.orders'),
stats: {
executionsBucket: '10-100',
distinctUsersBucket: '2-5',
@@ -159,7 +169,7 @@ describe('stageHistoricSqlAggregatedSnapshot', () => {
{
id: 'orders-by-status',
canonicalSql: expect.stringContaining('public.orders'),
- tablesTouched: ['public.customers', 'public.orders'],
+ tablesTouched: [tableRef('public.customers'), tableRef('public.orders')],
executionsBucket: '10-100',
distinctUsersBucket: '2-5',
dialect: 'postgres',
@@ -167,6 +177,129 @@ describe('stageHistoricSqlAggregatedSnapshot', () => {
]);
});
+ it('keeps templates when service-account topUsers are only a partial execution sample', async () => {
+ const stagedDir = await tempDir();
+ const reader: HistoricSqlReader = {
+ async probe() {
+ return { warnings: [], info: [] };
+ },
+ async *fetchAggregated() {
+ yield aggregate({
+ templateId: 'shared-bigquery-template',
+ canonicalSql: 'select status, count(*) from `demo.analytics.orders` group by status',
+ dialect: 'bigquery',
+ stats: {
+ executions: 42,
+ distinctUsers: 2,
+ firstSeen: '2026-05-01T00:00:00.000Z',
+ lastSeen: '2026-05-11T00:00:00.000Z',
+ p50RuntimeMs: 20,
+ p95RuntimeMs: 80,
+ errorRate: 0,
+ rowsProduced: null,
+ },
+ topUsers: [{ user: 'svc_loader', executions: 5 }],
+ });
+ },
+ };
+ const sqlAnalysis: SqlAnalysisPort = {
+ analyzeForFingerprint: vi.fn(),
+ analyzeBatch: vi.fn(async () =>
+ new Map([
+ [
+ 'shared-bigquery-template',
+ {
+ tablesTouched: [tableRef('demo.analytics.orders')],
+ columnsByClause: { select: ['status'], groupBy: ['status'] },
+ },
+ ],
+ ]),
+ ),
+ validateReadOnly: vi.fn(async () => ({ ok: true })),
+ };
+
+ await stageHistoricSqlAggregatedSnapshot({
+ stagedDir,
+ connectionId: 'warehouse',
+ queryClient: {},
+ reader,
+ sqlAnalysis,
+ pullConfig: {
+ dialect: 'bigquery',
+ windowDays: 90,
+ enabledSchemas: ['analytics'],
+ filters: {
+ serviceAccounts: { patterns: ['^svc_loader$'], mode: 'exclude' },
+ },
+ },
+ now: new Date('2026-05-11T12:00:00.000Z'),
+ });
+
+ const patterns = await readJson>(stagedDir, 'patterns-input.json');
+ expect(patterns.templates.map((template: { id: string }) => template.id)).toEqual([
+ 'shared-bigquery-template',
+ ]);
+ const orders = await readJson>(stagedDir, 'tables/demo.analytics.orders.json');
+ expect(orders.topTemplates).toEqual([
+ {
+ id: 'shared-bigquery-template',
+ canonicalSql: 'select status, count(*) from `demo.analytics.orders` group by status',
+ topUsers: [{ user: 'svc_loader' }],
+ },
+ ]);
+ });
+
+ it('drops service-account-only templates when matched users cover all executions', async () => {
+ const stagedDir = await tempDir();
+ const reader: HistoricSqlReader = {
+ async probe() {
+ return { warnings: [], info: [] };
+ },
+ async *fetchAggregated() {
+ yield aggregate({
+ templateId: 'service-only-template',
+ canonicalSql: 'merge into analytics.orders using staging.orders_delta on orders.id = orders_delta.id',
+ stats: {
+ executions: 12,
+ distinctUsers: 1,
+ firstSeen: '2026-05-01T00:00:00.000Z',
+ lastSeen: '2026-05-11T00:00:00.000Z',
+ p50RuntimeMs: 20,
+ p95RuntimeMs: 80,
+ errorRate: 0,
+ rowsProduced: 0,
+ },
+ topUsers: [{ user: 'svc_loader', executions: 12 }],
+ });
+ },
+ };
+ const sqlAnalysis: SqlAnalysisPort = {
+ analyzeForFingerprint: vi.fn(),
+ analyzeBatch: vi.fn(async () => new Map()),
+ validateReadOnly: vi.fn(async () => ({ ok: true })),
+ };
+
+ await stageHistoricSqlAggregatedSnapshot({
+ stagedDir,
+ connectionId: 'warehouse',
+ queryClient: {},
+ reader,
+ sqlAnalysis,
+ pullConfig: {
+ dialect: 'postgres',
+ enabledSchemas: ['analytics'],
+ filters: {
+ serviceAccounts: { patterns: ['^svc_loader$'], mode: 'exclude' },
+ },
+ },
+ now: new Date('2026-05-11T12:00:00.000Z'),
+ });
+
+ expect(sqlAnalysis.analyzeBatch).toHaveBeenCalledWith([], 'postgres', undefined);
+ const patterns = await readJson>(stagedDir, 'patterns-input.json');
+ expect(patterns.templates).toEqual([]);
+ });
+
it('redacts configured SQL substrings in staged artifacts while analyzing original SQL', async () => {
const stagedDir = await tempDir();
const originalSql =
@@ -198,7 +331,7 @@ describe('stageHistoricSqlAggregatedSnapshot', () => {
[
'api-events-with-secret',
{
- tablesTouched: ['public.api_events'],
+ tablesTouched: [tableRef('public.api_events')],
columnsByClause: {
select: [],
where: ['api_key', 'note'],
@@ -219,6 +352,7 @@ describe('stageHistoricSqlAggregatedSnapshot', () => {
sqlAnalysis,
pullConfig: {
dialect: 'postgres',
+ enabledSchemas: ['public'],
redactionPatterns: ['sk_live_[A-Za-z0-9]+', '(?i)secret_token_[a-z0-9]+'],
},
now: new Date('2026-05-11T12:00:00.000Z'),
@@ -227,6 +361,7 @@ describe('stageHistoricSqlAggregatedSnapshot', () => {
expect(sqlAnalysis.analyzeBatch).toHaveBeenCalledWith(
[{ id: 'api-events-with-secret', sql: originalSql }],
'postgres',
+ undefined,
);
const tableJson = await readFile(join(stagedDir, 'tables/public.api_events.json'), 'utf-8');
@@ -266,21 +401,21 @@ describe('stageHistoricSqlAggregatedSnapshot', () => {
[
'selected-qualified',
{
- tablesTouched: ['orbit_analytics.int_active_contract_arr'],
+ tablesTouched: [tableRef('orbit_analytics.int_active_contract_arr')],
columnsByClause: { select: [], where: [], join: [], groupBy: [] },
},
],
[
'selected-unqualified',
{
- tablesTouched: ['int_customer_health_signals'],
+ tablesTouched: [tableRef('orbit_analytics.int_customer_health_signals')],
columnsByClause: { select: [], where: [], join: [], groupBy: [] },
},
],
[
'unselected',
{
- tablesTouched: ['orbit_raw.accounts'],
+ tablesTouched: [tableRef('orbit_raw.accounts')],
columnsByClause: { select: [], where: [], join: [], groupBy: [] },
},
],
@@ -297,16 +432,16 @@ describe('stageHistoricSqlAggregatedSnapshot', () => {
pullConfig: {
dialect: 'postgres',
enabledTables: [
- 'orbit_analytics.int_active_contract_arr',
- 'orbit_analytics.int_customer_health_signals',
+ tableRef('orbit_analytics.int_active_contract_arr'),
+ tableRef('orbit_analytics.int_customer_health_signals'),
],
},
now: new Date('2026-05-11T12:00:00.000Z'),
});
expect(await readdir(join(stagedDir, 'tables'))).toEqual([
- 'int_customer_health_signals.json',
'orbit_analytics.int_active_contract_arr.json',
+ 'orbit_analytics.int_customer_health_signals.json',
]);
const manifest = await readJson>(stagedDir, 'manifest.json');
expect(manifest.touchedTableCount).toBe(2);
@@ -372,7 +507,7 @@ describe('stageHistoricSqlAggregatedSnapshot', () => {
[
'orders-customers-a',
{
- tablesTouched: ['public.orders', 'public.customers'],
+ tablesTouched: [tableRef('public.orders'), tableRef('public.customers')],
columnsByClause: {
select: [],
where: ['payload'],
@@ -384,7 +519,7 @@ describe('stageHistoricSqlAggregatedSnapshot', () => {
[
'orders-customers-b',
{
- tablesTouched: ['public.orders', 'public.customers'],
+ tablesTouched: [tableRef('public.orders'), tableRef('public.customers')],
columnsByClause: {
select: [],
where: ['payload_b'],
@@ -396,7 +531,7 @@ describe('stageHistoricSqlAggregatedSnapshot', () => {
[
'orders-single-table',
{
- tablesTouched: ['public.orders'],
+ tablesTouched: [tableRef('public.orders')],
columnsByClause: {
select: [],
where: [],
@@ -415,7 +550,7 @@ describe('stageHistoricSqlAggregatedSnapshot', () => {
queryClient: {},
reader,
sqlAnalysis,
- pullConfig: { dialect: 'postgres' },
+ pullConfig: { dialect: 'postgres', enabledSchemas: ['public'] },
now: new Date('2026-05-11T12:00:00.000Z'),
});
@@ -456,7 +591,13 @@ describe('stageHistoricSqlAggregatedSnapshot', () => {
const sqlAnalysis: SqlAnalysisPort = {
analyzeForFingerprint: vi.fn(),
analyzeBatch: vi.fn(async () => new Map([
- ['analytic', { tablesTouched: ['public.orders'], columnsByClause: { select: ['status'], where: [], join: [], groupBy: ['status'] } }],
+ [
+ 'analytic',
+ {
+ tablesTouched: [tableRef('public.orders')],
+ columnsByClause: { select: ['status'], where: [], join: [], groupBy: ['status'] },
+ },
+ ],
])),
validateReadOnly: vi.fn(async () => ({ ok: true })),
};
@@ -467,7 +608,7 @@ describe('stageHistoricSqlAggregatedSnapshot', () => {
queryClient: {},
reader,
sqlAnalysis,
- pullConfig: { dialect: 'postgres' },
+ pullConfig: { dialect: 'postgres', enabledSchemas: ['public'] },
now: new Date('2026-05-11T12:00:00.000Z'),
});
@@ -475,26 +616,27 @@ describe('stageHistoricSqlAggregatedSnapshot', () => {
expect(sqlAnalysis.analyzeBatch).toHaveBeenCalledWith(
[{ id: 'analytic', sql: 'select status, count(*) from public.orders group by status' }],
'postgres',
+ undefined,
);
expect(await readdir(join(stagedDir, 'tables'))).toEqual(['public.orders.json']);
});
- it('merges bare and schema-qualified references to the same table into one work unit', async () => {
+ it('keeps modeled-schema refs and drops unmodeled-schema refs by default', async () => {
const stagedDir = await tempDir();
const reader: HistoricSqlReader = {
async probe() {
return { warnings: [], info: [] };
},
async *fetchAggregated() {
- yield aggregate({ templateId: 'qualified', canonicalSql: 'select count(*) from orbit_raw.accounts' });
- yield aggregate({ templateId: 'bare', canonicalSql: 'select id from accounts where active' });
+ yield aggregate({ templateId: 'modeled', canonicalSql: 'select count(*) from orbit_raw.accounts' });
+ yield aggregate({ templateId: 'noise', canonicalSql: 'select count(*) from metabase.application_table' });
},
};
const sqlAnalysis: SqlAnalysisPort = {
analyzeForFingerprint: vi.fn(),
analyzeBatch: vi.fn(async () => new Map([
- ['qualified', { tablesTouched: ['orbit_raw.accounts'], columnsByClause: { select: [], where: [], join: [], groupBy: [] } }],
- ['bare', { tablesTouched: ['accounts'], columnsByClause: { select: ['id'], where: ['active'], join: [], groupBy: [] } }],
+ ['modeled', { tablesTouched: [{ catalog: null, db: 'orbit_raw', name: 'accounts' }], columnsByClause: {} }],
+ ['noise', { tablesTouched: [{ catalog: null, db: 'metabase', name: 'application_table' }], columnsByClause: {} }],
])),
validateReadOnly: vi.fn(async () => ({ ok: true })),
};
@@ -505,16 +647,213 @@ describe('stageHistoricSqlAggregatedSnapshot', () => {
queryClient: {},
reader,
sqlAnalysis,
- pullConfig: { dialect: 'postgres' },
+ pullConfig: {
+ dialect: 'postgres',
+ enabledSchemas: ['orbit_raw'],
+ modeledTableCatalog: [{ catalog: null, db: 'orbit_raw', name: 'accounts' }],
+ },
now: new Date('2026-05-11T12:00:00.000Z'),
});
- // The bare `accounts` reference resolves to the unique qualified `orbit_raw.accounts`,
- // so the two templates collapse into a single work unit instead of two.
expect(await readdir(join(stagedDir, 'tables'))).toEqual(['orbit_raw.accounts.json']);
- const merged = await readJson>(stagedDir, 'tables/orbit_raw.accounts.json');
- expect(merged.topTemplates.map((t: any) => t.id).sort()).toEqual(['bare', 'qualified']);
const manifest = await readJson>(stagedDir, 'manifest.json');
expect(manifest.touchedTableCount).toBe(1);
});
+
+ it('fails open when the implicit modeled scope is empty', async () => {
+ const stagedDir = await tempDir();
+ const reader: HistoricSqlReader = {
+ async probe() {
+ return { warnings: [], info: [] };
+ },
+ async *fetchAggregated() {
+ yield aggregate({ templateId: 'any-table', canonicalSql: 'select count(*) from metabase.application_table' });
+ },
+ };
+ const sqlAnalysis: SqlAnalysisPort = {
+ analyzeForFingerprint: vi.fn(),
+ analyzeBatch: vi.fn(async () => new Map([
+ ['any-table', { tablesTouched: [{ catalog: null, db: 'metabase', name: 'application_table' }], columnsByClause: {} }],
+ ])),
+ validateReadOnly: vi.fn(async () => ({ ok: true })),
+ };
+
+ await stageHistoricSqlAggregatedSnapshot({
+ stagedDir,
+ connectionId: 'warehouse',
+ queryClient: {},
+ reader,
+ sqlAnalysis,
+ pullConfig: { dialect: 'postgres', enabledSchemas: [], modeledTableCatalog: [] },
+ now: new Date('2026-05-11T12:00:00.000Z'),
+ });
+
+ expect(await readdir(join(stagedDir, 'tables'))).toEqual(['metabase.application_table.json']);
+ const manifest = await readJson>(stagedDir, 'manifest.json');
+ expect(manifest.warnings).toContain('query_history_scope_floor_disabled:empty_modeled_scope');
+ });
+
+ it('lets enabledSchemas star disable the floor', async () => {
+ const stagedDir = await tempDir();
+ const reader: HistoricSqlReader = {
+ async probe() {
+ return { warnings: [], info: [] };
+ },
+ async *fetchAggregated() {
+ yield aggregate({ templateId: 'noise', canonicalSql: 'select count(*) from metabase.application_table' });
+ },
+ };
+ const sqlAnalysis: SqlAnalysisPort = {
+ analyzeForFingerprint: vi.fn(),
+ analyzeBatch: vi.fn(async () => new Map([
+ ['noise', { tablesTouched: [{ catalog: null, db: 'metabase', name: 'application_table' }], columnsByClause: {} }],
+ ])),
+ validateReadOnly: vi.fn(async () => ({ ok: true })),
+ };
+
+ await stageHistoricSqlAggregatedSnapshot({
+ stagedDir,
+ connectionId: 'warehouse',
+ queryClient: {},
+ reader,
+ sqlAnalysis,
+ pullConfig: {
+ dialect: 'postgres',
+ enabledSchemas: ['*'],
+ modeledTableCatalog: [{ catalog: null, db: 'orbit_raw', name: 'accounts' }],
+ },
+ now: new Date('2026-05-11T12:00:00.000Z'),
+ });
+
+ expect(await readdir(join(stagedDir, 'tables'))).toEqual(['metabase.application_table.json']);
+ });
+
+ it('matches BigQuery dataset scope even when refs include a catalog', async () => {
+ const stagedDir = await tempDir();
+ const reader: HistoricSqlReader = {
+ async probe() {
+ return { warnings: [], info: [] };
+ },
+ async *fetchAggregated() {
+ yield aggregate({ templateId: 'modeled', canonicalSql: 'select count(*) from `demo-project.orbit_analytics.orders`' });
+ yield aggregate({ templateId: 'noise', canonicalSql: 'select count(*) from `demo-project.metabase.application_table`' });
+ },
+ };
+ const sqlAnalysis: SqlAnalysisPort = {
+ analyzeForFingerprint: vi.fn(),
+ analyzeBatch: vi.fn(async () => new Map([
+ ['modeled', { tablesTouched: [{ catalog: 'demo-project', db: 'orbit_analytics', name: 'orders' }], columnsByClause: {} }],
+ ['noise', { tablesTouched: [{ catalog: 'demo-project', db: 'metabase', name: 'application_table' }], columnsByClause: {} }],
+ ])),
+ validateReadOnly: vi.fn(async () => ({ ok: true })),
+ };
+
+ await stageHistoricSqlAggregatedSnapshot({
+ stagedDir,
+ connectionId: 'warehouse',
+ queryClient: {},
+ reader,
+ sqlAnalysis,
+ pullConfig: {
+ dialect: 'bigquery',
+ enabledSchemas: ['orbit_analytics'],
+ modeledTableCatalog: [{ catalog: 'demo-project', db: 'orbit_analytics', name: 'orders' }],
+ },
+ now: new Date('2026-05-11T12:00:00.000Z'),
+ });
+
+ expect(await readdir(join(stagedDir, 'tables'))).toEqual(['demo-project.orbit_analytics.orders.json']);
+ });
+
+ it('writes propagated scope-floor warnings to the staged manifest', async () => {
+ const stagedDir = await tempDir();
+ const reader: HistoricSqlReader = {
+ async probe() {
+ return { warnings: [], info: [] };
+ },
+ async *fetchAggregated() {
+ yield aggregate({ templateId: 'any-table', canonicalSql: 'select count(*) from metabase.application_table' });
+ },
+ };
+ const sqlAnalysis: SqlAnalysisPort = {
+ analyzeForFingerprint: vi.fn(),
+ analyzeBatch: vi.fn(async () => new Map([
+ ['any-table', { tablesTouched: [{ catalog: null, db: 'metabase', name: 'application_table' }], columnsByClause: {} }],
+ ])),
+ validateReadOnly: vi.fn(async () => ({ ok: true })),
+ };
+
+ await stageHistoricSqlAggregatedSnapshot({
+ stagedDir,
+ connectionId: 'warehouse',
+ queryClient: {},
+ reader,
+ sqlAnalysis,
+ pullConfig: {
+ dialect: 'postgres',
+ enabledSchemas: ['*'],
+ scopeFloorWarnings: ['query_history_scope_floor_disabled:catalog_unavailable'],
+ },
+ now: new Date('2026-05-11T12:00:00.000Z'),
+ });
+
+ const manifest = await readJson>(stagedDir, 'manifest.json');
+ expect(manifest.warnings).toContain('query_history_scope_floor_disabled:catalog_unavailable');
+ expect(await readdir(join(stagedDir, 'tables'))).toEqual(['metabase.application_table.json']);
+ });
+
+ it('retries without the catalog and disables the floor when catalog qualification fails wholesale', async () => {
+ const stagedDir = await tempDir();
+ const reader: HistoricSqlReader = {
+ async probe() {
+ return { warnings: [], info: [] };
+ },
+ async *fetchAggregated() {
+ yield aggregate({ templateId: 'noise', canonicalSql: 'select count(*) from metabase.application_table' });
+ },
+ };
+ const sqlAnalysis: SqlAnalysisPort = {
+ analyzeForFingerprint: vi.fn(),
+ analyzeBatch: vi
+ .fn()
+ .mockRejectedValueOnce(new Error('catalog qualification failed'))
+ .mockResolvedValueOnce(
+ new Map([
+ ['noise', { tablesTouched: [{ catalog: null, db: 'metabase', name: 'application_table' }], columnsByClause: {} }],
+ ]),
+ ),
+ validateReadOnly: vi.fn(async () => ({ ok: true })),
+ };
+
+ await stageHistoricSqlAggregatedSnapshot({
+ stagedDir,
+ connectionId: 'warehouse',
+ queryClient: {},
+ reader,
+ sqlAnalysis,
+ pullConfig: {
+ dialect: 'postgres',
+ enabledSchemas: ['orbit_raw'],
+ modeledTableCatalog: [{ catalog: null, db: 'orbit_raw', name: 'accounts' }],
+ },
+ now: new Date('2026-05-11T12:00:00.000Z'),
+ });
+
+ expect(sqlAnalysis.analyzeBatch).toHaveBeenCalledTimes(2);
+ expect(sqlAnalysis.analyzeBatch).toHaveBeenNthCalledWith(
+ 1,
+ [{ id: 'noise', sql: 'select count(*) from metabase.application_table' }],
+ 'postgres',
+ { catalog: { tables: [{ catalog: null, db: 'orbit_raw', name: 'accounts' }] } },
+ );
+ expect(sqlAnalysis.analyzeBatch).toHaveBeenNthCalledWith(
+ 2,
+ [{ id: 'noise', sql: 'select count(*) from metabase.application_table' }],
+ 'postgres',
+ undefined,
+ );
+ expect(await readdir(join(stagedDir, 'tables'))).toEqual(['metabase.application_table.json']);
+ const manifest = await readJson>(stagedDir, 'manifest.json');
+ expect(manifest.warnings).toContain('query_history_scope_floor_disabled:catalog_qualification_failed');
+ });
});
diff --git a/packages/cli/test/context/ingest/adapters/historic-sql/types.test.ts b/packages/cli/test/context/ingest/adapters/historic-sql/types.test.ts
index d9417521..ca3d7e70 100644
--- a/packages/cli/test/context/ingest/adapters/historic-sql/types.test.ts
+++ b/packages/cli/test/context/ingest/adapters/historic-sql/types.test.ts
@@ -59,6 +59,7 @@ describe('historic-sql unified contracts', () => {
expect(
stagedTableInputSchema.parse({
table: 'public.orders',
+ tableRef: { catalog: null, db: 'public', name: 'orders' },
stats: {
executionsBucket: '10-100',
distinctUsersBucket: '2-5',
@@ -81,7 +82,7 @@ describe('historic-sql unified contracts', () => {
{
id: 'pg:123',
canonicalSql: 'select * from public.orders',
- tablesTouched: ['public.orders'],
+ tablesTouched: [{ catalog: null, db: 'public', name: 'orders' }],
executionsBucket: '10-100',
distinctUsersBucket: '2-5',
dialect: 'postgres',
diff --git a/packages/cli/test/context/ingest/local-adapters.test.ts b/packages/cli/test/context/ingest/local-adapters.test.ts
index f70c4879..a8799cee 100644
--- a/packages/cli/test/context/ingest/local-adapters.test.ts
+++ b/packages/cli/test/context/ingest/local-adapters.test.ts
@@ -1,4 +1,4 @@
-import { mkdtemp, rm, writeFile } from 'node:fs/promises';
+import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
@@ -34,6 +34,36 @@ describe('local ingest adapters', () => {
};
}
+ async function seedLiveScanTable(
+ projectDir: string,
+ connectionId: string,
+ table: { catalog: string | null; db: string | null; name: string },
+ ): Promise {
+ const rawRoot = join(projectDir, 'raw-sources', connectionId, 'live-database', 'sync-1');
+ await mkdir(join(rawRoot, 'tables'), { recursive: true });
+ await writeFile(
+ join(rawRoot, 'connection.json'),
+ `${JSON.stringify({ connectionId, driver: 'postgres' }, null, 2)}\n`,
+ 'utf-8',
+ );
+ await writeFile(
+ join(rawRoot, 'tables', `${table.db ?? 'default'}-${table.name}.json`),
+ `${JSON.stringify(
+ {
+ ...table,
+ kind: 'table',
+ comment: null,
+ estimatedRows: null,
+ columns: [],
+ foreignKeys: [],
+ },
+ null,
+ 2,
+ )}\n`,
+ 'utf-8',
+ );
+ }
+
it('registers Metabase locally as a staged-bundle adapter', () => {
const adapters = createDefaultLocalIngestAdapters(project);
@@ -205,11 +235,14 @@ describe('local ingest adapters', () => {
dialect: 'postgres',
minExecutions: 7,
enabledTables: [],
+ enabledSchemas: [],
+ modeledTableCatalog: [],
filters: {
serviceAccounts: { patterns: ['^svc_'], mode: 'exclude' },
dropTrivialProbes: true,
},
redactionPatterns: [],
+ scopeFloorWarnings: [],
staleArchiveAfterDays: 90,
});
});
@@ -237,6 +270,71 @@ describe('local ingest adapters', () => {
});
});
+ it('passes computed modeled scope to direct historic-sql adapter pull config', async () => {
+ await mkdir(join(project.projectDir, 'semantic-layer/warehouse'), { recursive: true });
+ await writeFile(
+ join(project.projectDir, 'semantic-layer/warehouse/revenue.yaml'),
+ [
+ 'name: revenue',
+ 'table: orbit_analytics.mart_revenue',
+ 'grain: [id]',
+ 'columns:',
+ ' - name: id',
+ ' type: string',
+ '',
+ ].join('\n'),
+ 'utf-8',
+ );
+ await seedLiveScanTable(project.projectDir, 'warehouse', {
+ catalog: null,
+ db: 'orbit_raw',
+ name: 'accounts',
+ });
+ const projectWithQueryHistory = projectWithConnections({
+ warehouse: {
+ driver: 'postgres',
+ schemas: ['orbit_raw'],
+ context: {
+ queryHistory: {
+ enabled: true,
+ minExecutions: 7,
+ filters: { dropTrivialProbes: true },
+ },
+ },
+ },
+ });
+ const adapter = { source: 'historic-sql' } as never;
+
+ await expect(localPullConfigForAdapter(projectWithQueryHistory, adapter, 'warehouse')).resolves.toMatchObject({
+ dialect: 'postgres',
+ minExecutions: 7,
+ enabledSchemas: ['orbit_analytics', 'orbit_raw'],
+ modeledTableCatalog: [
+ { catalog: null, db: 'orbit_analytics', name: 'mart_revenue' },
+ { catalog: null, db: 'orbit_raw', name: 'accounts' },
+ ],
+ });
+ });
+
+ it('passes query-history scope fail-open warnings to direct historic-sql pull config', async () => {
+ const projectDir = await mkdtemp(join(tmpdir(), 'ktx-local-qh-scope-warning-'));
+ const project = await initKtxProject({ projectDir });
+ project.config.connections.warehouse = {
+ driver: 'postgres',
+ schemas: ['orbit_raw'],
+ context: { queryHistory: { enabled: true } },
+ } as never;
+ const adapter = { source: 'historic-sql' } as never;
+
+ await expect(localPullConfigForAdapter(project, adapter, 'warehouse')).resolves.toMatchObject({
+ dialect: 'postgres',
+ enabledSchemas: ['*'],
+ scopeFloorWarnings: ['query_history_scope_floor_disabled:catalog_unavailable'],
+ });
+
+ await rm(projectDir, { recursive: true, force: true });
+ });
+
it('rejects local historic-sql pulls when the connection has not enabled historic SQL', async () => {
const historicSql = createDefaultLocalIngestAdapters(project, {
historicSql: {
diff --git a/packages/cli/test/context/sql-analysis/http-sql-analysis-port.test.ts b/packages/cli/test/context/sql-analysis/http-sql-analysis-port.test.ts
index 02b275a6..df32fb8d 100644
--- a/packages/cli/test/context/sql-analysis/http-sql-analysis-port.test.ts
+++ b/packages/cli/test/context/sql-analysis/http-sql-analysis-port.test.ts
@@ -49,7 +49,10 @@ describe('createHttpSqlAnalysisPort', () => {
const requestJson = vi.fn(async () => ({
results: {
orders: {
- tables_touched: ['public.orders', 'public.customers'],
+ tables_touched: [
+ { catalog: null, db: 'public', name: 'orders' },
+ { catalog: null, db: 'public', name: 'customers' },
+ ],
columns_by_clause: {
select: ['status'],
where: ['created_at'],
@@ -79,7 +82,10 @@ describe('createHttpSqlAnalysisPort', () => {
[
'orders',
{
- tablesTouched: ['public.orders', 'public.customers'],
+ tablesTouched: [
+ { catalog: null, db: 'public', name: 'orders' },
+ { catalog: null, db: 'public', name: 'customers' },
+ ],
columnsByClause: {
select: ['status'],
where: ['created_at'],
@@ -108,6 +114,62 @@ describe('createHttpSqlAnalysisPort', () => {
});
});
+ it('passes an optional catalog and maps structured table refs for SQL batch analysis', async () => {
+ const requestJson = vi.fn(async () => ({
+ results: {
+ orders: {
+ tables_touched: [
+ { catalog: null, db: 'orbit_raw', name: 'accounts' },
+ { catalog: 'demo_project', db: 'orbit_analytics', name: 'orders' },
+ ],
+ columns_by_clause: { select: ['id'] },
+ error: null,
+ },
+ },
+ }));
+ const port = createHttpSqlAnalysisPort({ baseUrl: 'http://python.test', requestJson });
+
+ await expect(
+ port.analyzeBatch(
+ [{ id: 'orders', sql: 'select id from accounts' }],
+ 'postgres',
+ {
+ catalog: {
+ tables: [
+ { catalog: null, db: 'orbit_raw', name: 'accounts', columns: ['id'] },
+ { catalog: 'demo_project', db: 'orbit_analytics', name: 'orders', columns: ['id'] },
+ ],
+ },
+ },
+ ),
+ ).resolves.toEqual(
+ new Map([
+ [
+ 'orders',
+ {
+ tablesTouched: [
+ { catalog: null, db: 'orbit_raw', name: 'accounts' },
+ { catalog: 'demo_project', db: 'orbit_analytics', name: 'orders' },
+ ],
+ columnsByClause: { select: ['id'] },
+ error: null,
+ },
+ ],
+ ]),
+ );
+
+ expect(requestJson).toHaveBeenCalledWith('/sql/analyze-batch', {
+ dialect: 'postgres',
+ items: [{ id: 'orders', sql: 'select id from accounts' }],
+ catalog: {
+ tables: [
+ { catalog: null, db: 'orbit_raw', name: 'accounts', columns: ['id'] },
+ { catalog: 'demo_project', db: 'orbit_analytics', name: 'orders', columns: ['id'] },
+ ],
+ },
+ });
+ });
+
it('maps read-only SQL validation responses', async () => {
const requests: Array<{ path: string; payload: Record }> = [];
const port = createHttpSqlAnalysisPort({
@@ -150,7 +212,7 @@ describe('createHttpSqlAnalysisPort', () => {
const requestJson = vi.fn(async () => ({
results: {
orders: {
- tables_touched: ['public.orders'],
+ tables_touched: [{ catalog: null, db: 'public', name: 'orders' }],
columns_by_clause: { select: ['status'], where: [42] },
error: null,
},
diff --git a/packages/cli/test/local-adapters.test.ts b/packages/cli/test/local-adapters.test.ts
index 345f662a..467c9a56 100644
--- a/packages/cli/test/local-adapters.test.ts
+++ b/packages/cli/test/local-adapters.test.ts
@@ -2,8 +2,8 @@ import { mkdtemp, rm, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { loadKtxProject } from '../src/context/project/project.js';
-import { afterEach, beforeEach, describe, expect, it } from 'vitest';
-import { createKtxCliLocalIngestAdapters } from '../src/local-adapters.js';
+import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
+import { createKtxCliHistoricSqlRuntime, createKtxCliLocalIngestAdapters } from '../src/local-adapters.js';
function sqlAnalysisStub() {
return {
@@ -70,6 +70,116 @@ describe('CLI local ingest adapters', () => {
]);
});
+ it('creates reusable query-history runtime dependencies for setup', async () => {
+ await writeProject(
+ tempDir,
+ [
+ 'connections:',
+ ' warehouse:',
+ ' driver: postgres',
+ ' url: env:WAREHOUSE_DATABASE_URL',
+ ' readonly: true',
+ ' context:',
+ ' queryHistory:',
+ ' enabled: true',
+ '',
+ ].join('\n'),
+ );
+ const project = await loadKtxProject({ projectDir: tempDir });
+ const sqlAnalysis = sqlAnalysisStub();
+
+ const runtime = createKtxCliHistoricSqlRuntime(project, 'warehouse', { sqlAnalysis });
+
+ expect(runtime).toMatchObject({
+ dialect: 'postgres',
+ sqlAnalysis,
+ });
+ expect(runtime?.reader).toBeDefined();
+ expect(runtime?.queryClient).toBeDefined();
+ });
+
+ it('uses managed daemon SQL analysis when query-history runtime gets managed daemon options', async () => {
+ await writeProject(
+ tempDir,
+ [
+ 'connections:',
+ ' warehouse:',
+ ' driver: postgres',
+ ' url: env:WAREHOUSE_DATABASE_URL',
+ ' readonly: true',
+ ' context:',
+ ' queryHistory:',
+ ' enabled: true',
+ '',
+ ].join('\n'),
+ );
+ const project = await loadKtxProject({ projectDir: tempDir });
+ const testIo = {
+ stdout: { write: vi.fn() },
+ stderr: { write: vi.fn() },
+ };
+ const ensureRuntime = vi.fn(async () => ({
+ layout: {} as never,
+ manifest: {} as never,
+ }));
+ const startDaemon = vi.fn(async () => ({
+ status: 'started' as const,
+ layout: {} as never,
+ state: { pid: 1234 } as never,
+ baseUrl: 'http://127.0.0.1:61234',
+ }));
+ const postJson = vi.fn(async () => ({
+ results: {
+ probe: {
+ tables_touched: [],
+ columns_by_clause: {},
+ error: null,
+ },
+ },
+ }));
+
+ const runtime = createKtxCliHistoricSqlRuntime(project, 'warehouse', {
+ managedDaemon: {
+ cliVersion: '0.2.0',
+ projectDir: tempDir,
+ installPolicy: 'auto',
+ io: testIo,
+ ensureRuntime,
+ startDaemon,
+ postJson,
+ },
+ });
+
+ await expect(runtime?.sqlAnalysis.analyzeBatch([{ id: 'probe', sql: 'select 1' }], 'postgres')).resolves.toEqual(
+ new Map([
+ [
+ 'probe',
+ {
+ tablesTouched: [],
+ columnsByClause: {},
+ error: null,
+ },
+ ],
+ ]),
+ );
+ expect(ensureRuntime).toHaveBeenCalledWith({
+ cliVersion: '0.2.0',
+ installPolicy: 'auto',
+ io: testIo,
+ feature: 'core',
+ });
+ expect(startDaemon).toHaveBeenCalledWith({
+ cliVersion: '0.2.0',
+ projectDir: tempDir,
+ features: ['core'],
+ force: false,
+ });
+ expect(postJson).toHaveBeenCalledWith('http://127.0.0.1:61234', '/sql/analyze-batch', {
+ dialect: 'postgres',
+ items: [{ id: 'probe', sql: 'select 1' }],
+ });
+ });
+
it('registers historic SQL when explicitly requested even if connection query history is disabled', async () => {
await writeProject(
tempDir,
diff --git a/packages/cli/test/managed-python-http.test.ts b/packages/cli/test/managed-python-http.test.ts
index f19b6d72..74334042 100644
--- a/packages/cli/test/managed-python-http.test.ts
+++ b/packages/cli/test/managed-python-http.test.ts
@@ -161,7 +161,7 @@ describe('KTX daemon ingest ports', () => {
const requestJson = vi.fn(async () => ({
results: {
orders: {
- tables_touched: ['public.orders'],
+ tables_touched: [{ catalog: null, db: 'public', name: 'orders' }],
columns_by_clause: { select: ['status'] },
error: null,
},
@@ -175,7 +175,7 @@ describe('KTX daemon ingest ports', () => {
[
'orders',
{
- tablesTouched: ['public.orders'],
+ tablesTouched: [{ catalog: null, db: 'public', name: 'orders' }],
columnsByClause: { select: ['status'] },
error: null,
},
diff --git a/packages/cli/test/public-ingest.test.ts b/packages/cli/test/public-ingest.test.ts
index 1a8b457e..ba35faf6 100644
--- a/packages/cli/test/public-ingest.test.ts
+++ b/packages/cli/test/public-ingest.test.ts
@@ -1,4 +1,4 @@
-import { mkdtemp, rm } from 'node:fs/promises';
+import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { buildDefaultKtxProjectConfig, type KtxProjectConfig } from '../src/context/project/config.js';
@@ -668,12 +668,134 @@ describe('runKtxPublicIngest', () => {
dropFailedBelow: { errorRate: 0.5, executions: 3 },
},
redactionPatterns: ['(?i)secret'],
- enabledTables: ['orbit_analytics.int_active_contract_arr'],
+ enabledTables: [{ catalog: null, db: 'orbit_analytics', name: 'int_active_contract_arr' }],
},
});
expect(ingestArgs?.historicSqlPullConfigOverride).not.toHaveProperty('enabled');
});
+ it('resolves query-history scope after the schema scan writes artifacts', async () => {
+ const io = makeIo();
+ const projectDir = await mkdtemp(join(tmpdir(), 'ktx-public-qh-scope-'));
+ const project = deepReadyProject({
+ warehouse: {
+ driver: 'postgres',
+ schemas: ['orbit_raw'],
+ context: { queryHistory: { enabled: true } },
+ },
+ });
+ const runScan = vi.fn(async () => {
+ await mkdir(join(projectDir, 'semantic-layer/warehouse'), { recursive: true });
+ await writeFile(
+ join(projectDir, 'semantic-layer/warehouse/revenue.yaml'),
+ [
+ 'name: revenue',
+ 'table: orbit_analytics.mart_revenue',
+ 'grain: [id]',
+ 'columns:',
+ ' - name: id',
+ ' type: string',
+ '',
+ ].join('\n'),
+ 'utf-8',
+ );
+ const rawRoot = join(projectDir, 'raw-sources/warehouse/live-database/sync-1');
+ await mkdir(join(rawRoot, 'tables'), { recursive: true });
+ await writeFile(
+ join(rawRoot, 'connection.json'),
+ `${JSON.stringify({ connectionId: 'warehouse', driver: 'postgres' }, null, 2)}\n`,
+ 'utf-8',
+ );
+ await writeFile(
+ join(rawRoot, 'tables/accounts.json'),
+ `${JSON.stringify(
+ {
+ catalog: null,
+ db: 'orbit_raw',
+ name: 'accounts',
+ kind: 'table',
+ comment: null,
+ estimatedRows: null,
+ columns: [
+ {
+ name: 'id',
+ nativeType: 'integer',
+ normalizedType: 'integer',
+ dimensionType: 'number',
+ nullable: false,
+ primaryKey: true,
+ comment: null,
+ },
+ ],
+ foreignKeys: [],
+ },
+ null,
+ 2,
+ )}\n`,
+ 'utf-8',
+ );
+ await writeFile(
+ join(rawRoot, 'scan-report.json'),
+ `${JSON.stringify(
+ {
+ connectionId: 'warehouse',
+ driver: 'postgres',
+ syncId: 'sync-1',
+ runId: 'scan-sync-1',
+ trigger: 'cli',
+ mode: 'enriched',
+ dryRun: false,
+ artifactPaths: {
+ rawSourcesDir: 'raw-sources/warehouse/live-database/sync-1',
+ reportPath: 'raw-sources/warehouse/live-database/sync-1/scan-report.json',
+ manifestShards: [],
+ enrichmentArtifacts: [],
+ },
+ counts: {},
+ warnings: [],
+ enrichment: {},
+ enrichmentState: {},
+ },
+ null,
+ 2,
+ )}\n`,
+ 'utf-8',
+ );
+ return 0;
+ });
+ const runIngest = vi.fn>(async () => 0);
+
+ await expect(
+ runKtxPublicIngest(
+ {
+ command: 'run',
+ projectDir,
+ targetConnectionId: 'warehouse',
+ all: false,
+ json: false,
+ inputMode: 'disabled',
+ queryHistory: 'enabled',
+ },
+ io.io,
+ { loadProject: vi.fn(async () => ({ ...project, projectDir })), runScan, runIngest },
+ ),
+ ).resolves.toBe(0);
+
+ const ingestArgs = runIngest.mock.calls[0]?.[0] as
+ | Extract>[0], { command: 'run' }>
+ | undefined;
+ expect(ingestArgs?.historicSqlPullConfigOverride).toMatchObject({
+ dialect: 'postgres',
+ enabledSchemas: ['orbit_analytics', 'orbit_raw'],
+ modeledTableCatalog: [
+ { catalog: null, db: 'orbit_analytics', name: 'mart_revenue' },
+ { catalog: null, db: 'orbit_raw', name: 'accounts' },
+ ],
+ });
+
+ await rm(projectDir, { recursive: true, force: true });
+ });
+
it('prints the schema-first notice for explicit query-history runs', async () => {
const io = makeIo();
const project = deepReadyProject({
diff --git a/packages/cli/test/setup-databases.test.ts b/packages/cli/test/setup-databases.test.ts
index 265459e2..6adb0af0 100644
--- a/packages/cli/test/setup-databases.test.ts
+++ b/packages/cli/test/setup-databases.test.ts
@@ -6,6 +6,7 @@ import { parseKtxProjectConfig } from '../src/context/project/config.js';
import { readKtxSetupState, writeKtxSetupState } from '../src/context/project/setup-config.js';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import {
+ managedDaemonOptionsForSetupQueryHistoryPicker,
type KtxSetupDatabaseDriver,
type KtxSetupDatabasesDeps,
type KtxSetupDatabasesPromptAdapter,
@@ -137,6 +138,22 @@ function textInputPrompt(message: string): string {
return `${title}\n│\n│ ${bodyLines.join('\n│ ')}\n│ Press Escape to go back.\n│`;
}
+function queryHistoryFromConfig(connection: unknown): {
+ filters?: { serviceAccounts?: unknown; dropTrivialProbes?: boolean };
+} | undefined {
+ if (!connection || typeof connection !== 'object' || Array.isArray(connection)) {
+ return undefined;
+ }
+ const context = (connection as { context?: unknown }).context;
+ if (!context || typeof context !== 'object' || Array.isArray(context)) {
+ return undefined;
+ }
+ const queryHistory = (context as { queryHistory?: unknown }).queryHistory;
+ return queryHistory && typeof queryHistory === 'object' && !Array.isArray(queryHistory)
+ ? (queryHistory as { filters?: { serviceAccounts?: unknown; dropTrivialProbes?: boolean } })
+ : undefined;
+}
+
describe('setup databases step', () => {
let tempDir: string;
@@ -150,6 +167,61 @@ describe('setup databases step', () => {
await rm(tempDir, { recursive: true, force: true });
});
+ it('builds managed daemon options for setup query-history SQL analysis', () => {
+ const io = makeIo();
+
+ expect(
+ managedDaemonOptionsForSetupQueryHistoryPicker({
+ projectDir: tempDir,
+ args: {
+ inputMode: 'disabled',
+ cliVersion: '0.2.0',
+ runtimeInstallPolicy: 'auto',
+ },
+ io: io.io,
+ }),
+ ).toEqual({
+ cliVersion: '0.2.0',
+ projectDir: tempDir,
+ installPolicy: 'auto',
+ io: io.io,
+ });
+ });
+
+ it('defaults managed daemon setup options when the database step is called directly', () => {
+ const io = makeIo();
+
+ expect(
+ managedDaemonOptionsForSetupQueryHistoryPicker({
+ projectDir: tempDir,
+ args: {
+ inputMode: 'disabled',
+ },
+ io: io.io,
+ }),
+ ).toMatchObject({
+ cliVersion: expect.any(String),
+ projectDir: tempDir,
+ installPolicy: 'never',
+ io: io.io,
+ });
+
+ expect(
+ managedDaemonOptionsForSetupQueryHistoryPicker({
+ projectDir: tempDir,
+ args: {
+ inputMode: 'auto',
+ },
+ io: io.io,
+ }),
+ ).toMatchObject({
+ cliVersion: expect.any(String),
+ projectDir: tempDir,
+ installPolicy: 'prompt',
+ io: io.io,
+ });
+ });
+
it('shows every supported database in the interactive checklist', async () => {
const prompts = makePromptAdapter({ multiselectValues: [['back']] });
@@ -2569,6 +2641,190 @@ describe('setup databases step', () => {
expect(io.stdout()).toContain('pg_stat_statements ready');
});
+ it('auto-applies derived query-history service-account filters in non-interactive setup', async () => {
+ const io = makeIo();
+ const queryHistoryFilterPicker = vi.fn(async () => ({
+ excludedRoles: [
+ {
+ role: 'svc_loader',
+ pattern: '^svc_loader$',
+ reason: 'Runs recurring loader traffic against modeled tables.',
+ },
+ ],
+ consideredRoleCount: 2,
+ skipped: null,
+ warnings: [],
+ }));
+
+ const result = await runKtxSetupDatabasesStep(
+ {
+ projectDir: tempDir,
+ inputMode: 'disabled',
+ yes: true,
+ databaseDrivers: ['postgres'],
+ databaseConnectionId: 'warehouse',
+ databaseUrl: 'env:DATABASE_URL',
+ databaseSchemas: ['public'],
+ enableQueryHistory: true,
+ skipDatabases: false,
+ },
+ io.io,
+ {
+ testConnection: vi.fn(async () => 0),
+ scanConnection: vi.fn(async () => 0),
+ historicSqlReadinessProbe: vi.fn(async () => {
+ const runner = fakeHistoricSqlRunner('postgres', 'pg_stat_statements');
+ return {
+ ok: true as const,
+ dialect: 'postgres' as const,
+ runner,
+ result: { pgServerVersion: 'PostgreSQL 16.4', warnings: [], info: [] },
+ };
+ }),
+ queryHistoryFilterPicker,
+ createQueryHistoryLlmRuntime: vi.fn(() => null),
+ },
+ );
+
+ expect(result.status).toBe('ready');
+ expect(queryHistoryFilterPicker).toHaveBeenCalledTimes(1);
+ const config = parseKtxProjectConfig(await readFile(join(tempDir, 'ktx.yaml'), 'utf-8'));
+ expect(config.connections.warehouse).toMatchObject({
+ context: {
+ queryHistory: {
+ filters: {
+ dropTrivialProbes: true,
+ serviceAccounts: {
+ mode: 'exclude',
+ patterns: ['^svc_loader$'],
+ },
+ },
+ },
+ },
+ });
+ expect(io.stdout()).toContain('Proposed query-history service-account filters');
+ expect(io.stdout()).toContain('svc_loader');
+ });
+
+ it('lets interactive setup skip applying derived filters', async () => {
+ const io = makeIo();
+ const prompts = makePromptAdapter({
+ selectValues: ['skip'],
+ });
+
+ const result = await runKtxSetupDatabasesStep(
+ {
+ projectDir: tempDir,
+ inputMode: 'auto',
+ yes: false,
+ databaseDrivers: ['postgres'],
+ databaseConnectionId: 'warehouse',
+ databaseUrl: 'env:DATABASE_URL',
+ databaseSchemas: ['public'],
+ enableQueryHistory: true,
+ skipDatabases: false,
+ },
+ io.io,
+ {
+ prompts,
+ testConnection: vi.fn(async () => 0),
+ scanConnection: vi.fn(async () => 0),
+ historicSqlReadinessProbe: vi.fn(async () => {
+ const runner = fakeHistoricSqlRunner('postgres', 'pg_stat_statements');
+ return {
+ ok: true as const,
+ dialect: 'postgres' as const,
+ runner,
+ result: { pgServerVersion: 'PostgreSQL 16.4', warnings: [], info: [] },
+ };
+ }),
+ queryHistoryFilterPicker: vi.fn(async () => ({
+ excludedRoles: [{ role: 'svc_loader', pattern: '^svc_loader$', reason: 'Loader traffic.' }],
+ consideredRoleCount: 2,
+ skipped: null,
+ warnings: [],
+ })),
+ createQueryHistoryLlmRuntime: vi.fn(() => null),
+ },
+ );
+
+ expect(result.status).toBe('ready');
+ const config = parseKtxProjectConfig(await readFile(join(tempDir, 'ktx.yaml'), 'utf-8'));
+ expect(queryHistoryFromConfig(config.connections.warehouse)?.filters).toEqual({ dropTrivialProbes: true });
+ expect(prompts.select).toHaveBeenCalledWith({
+ message: 'Apply 1 derived query-history service-account exclusion?',
+ options: [
+ { value: 'apply', label: 'Apply derived filters (recommended)' },
+ { value: 'skip', label: 'Leave query history filters unchanged' },
+ ],
+ });
+ });
+
+ it('does not overwrite an existing serviceAccounts block', async () => {
+ await writeFile(
+ join(tempDir, 'ktx.yaml'),
+ [
+ 'connections:',
+ ' warehouse:',
+ ' driver: postgres',
+ ' url: env:DATABASE_URL',
+ ' context:',
+ ' queryHistory:',
+ ' enabled: true',
+ ' filters:',
+ ' dropTrivialProbes: true',
+ ' serviceAccounts:',
+ ' mode: exclude',
+ ' patterns:',
+ " - '^existing$'",
+ '',
+ ].join('\n'),
+ 'utf-8',
+ );
+
+ const io = makeIo();
+ const result = await runKtxSetupDatabasesStep(
+ {
+ projectDir: tempDir,
+ inputMode: 'disabled',
+ yes: true,
+ databaseConnectionIds: ['warehouse'],
+ databaseSchemas: [],
+ enableQueryHistory: true,
+ skipDatabases: false,
+ },
+ io.io,
+ {
+ testConnection: vi.fn(async () => 0),
+ scanConnection: vi.fn(async () => 0),
+ historicSqlReadinessProbe: vi.fn(async () => {
+ const runner = fakeHistoricSqlRunner('postgres', 'pg_stat_statements');
+ return {
+ ok: true as const,
+ dialect: 'postgres' as const,
+ runner,
+ result: { pgServerVersion: 'PostgreSQL 16.4', warnings: [], info: [] },
+ };
+ }),
+ queryHistoryFilterPicker: vi.fn(async () => ({
+ excludedRoles: [{ role: 'svc_loader', pattern: '^svc_loader$', reason: 'Loader traffic.' }],
+ consideredRoleCount: 2,
+ skipped: { reason: 'user-block-present' as const },
+ warnings: [],
+ })),
+ createQueryHistoryLlmRuntime: vi.fn(() => null),
+ },
+ );
+
+ expect(result.status).toBe('ready');
+ const config = parseKtxProjectConfig(await readFile(join(tempDir, 'ktx.yaml'), 'utf-8'));
+ expect(queryHistoryFromConfig(config.connections.warehouse)?.filters?.serviceAccounts).toEqual({
+ mode: 'exclude',
+ patterns: ['^existing$'],
+ });
+ expect(io.stdout()).toContain('Existing query-history service-account filters left unchanged');
+ });
+
it('asks interactive Postgres setup whether to enable query history', async () => {
await writeFile(
join(tempDir, 'ktx.yaml'),
diff --git a/packages/cli/test/setup.test.ts b/packages/cli/test/setup.test.ts
index e4eca44d..9b8bf689 100644
--- a/packages/cli/test/setup.test.ts
+++ b/packages/cli/test/setup.test.ts
@@ -1684,6 +1684,9 @@ describe('setup status', () => {
expect.objectContaining({
projectDir: tempDir,
inputMode: 'disabled',
+ yes: true,
+ cliVersion: '0.2.0',
+ runtimeInstallPolicy: 'auto',
databaseDrivers: ['postgres'],
databaseConnectionId: 'warehouse',
databaseUrl: 'env:DATABASE_URL',
diff --git a/packages/cli/test/sql.test.ts b/packages/cli/test/sql.test.ts
index b48ebe5b..ef74fd49 100644
--- a/packages/cli/test/sql.test.ts
+++ b/packages/cli/test/sql.test.ts
@@ -33,7 +33,7 @@ function makeIo(options: { isTTY?: boolean } = {}) {
function makeSqlAnalysis(result: Awaited>): SqlAnalysisPort {
return {
analyzeForFingerprint: vi.fn(),
- analyzeBatch: vi.fn(async () => new Map([['cli-sql', { tablesTouched: ['orders'], columnsByClause: {} }]])),
+ analyzeBatch: vi.fn(async () => new Map([['cli-sql', { tablesTouched: [{ catalog: null, db: null, name: 'orders' }], columnsByClause: {} }]])),
validateReadOnly: vi.fn(async () => result),
};
}
diff --git a/python/ktx-daemon/src/ktx_daemon/sql_analysis.py b/python/ktx-daemon/src/ktx_daemon/sql_analysis.py
index e831e47f..54f3d0e2 100644
--- a/python/ktx-daemon/src/ktx_daemon/sql_analysis.py
+++ b/python/ktx-daemon/src/ktx_daemon/sql_analysis.py
@@ -2,15 +2,32 @@ from __future__ import annotations
import os
from concurrent.futures import ProcessPoolExecutor
+from dataclasses import dataclass
from typing import Literal
import sqlglot
from pydantic import BaseModel, Field
from sqlglot import exp
+from sqlglot.optimizer.normalize_identifiers import normalize_identifiers
+from sqlglot.optimizer.qualify_tables import qualify_tables
SqlAnalysisClause = Literal["select", "where", "join", "groupBy", "having", "orderBy"]
+class SqlAnalysisTableRef(BaseModel):
+ catalog: str | None = None
+ db: str | None = None
+ name: str
+
+
+class SqlAnalysisCatalogTable(SqlAnalysisTableRef):
+ columns: list[str] = Field(default_factory=list)
+
+
+class AnalyzeSqlCatalog(BaseModel):
+ tables: list[SqlAnalysisCatalogTable] = Field(default_factory=list)
+
+
class AnalyzeSqlBatchItem(BaseModel):
id: str
sql: str
@@ -19,11 +36,12 @@ class AnalyzeSqlBatchItem(BaseModel):
class AnalyzeSqlBatchRequest(BaseModel):
dialect: str
items: list[AnalyzeSqlBatchItem]
+ catalog: AnalyzeSqlCatalog | None = None
max_workers: int | None = Field(default=None, ge=1, le=32)
class AnalyzeSqlBatchResult(BaseModel):
- tables_touched: list[str] = Field(default_factory=list)
+ tables_touched: list[SqlAnalysisTableRef] = Field(default_factory=list)
columns_by_clause: dict[SqlAnalysisClause, list[str]] = Field(default_factory=dict)
error: str | None = None
@@ -82,17 +100,76 @@ def _ordered_unique(values: list[str]) -> list[str]:
return result
-def _table_ref(table: exp.Table) -> str:
- parts: list[str] = []
+def _normalize_identifier(value: str | None, dialect: str) -> str | None:
+ if value is None:
+ return None
+ identifier = exp.to_identifier(value)
+ identifier.meta["is_table"] = True
+ normalized = normalize_identifiers(identifier, dialect=dialect)
+ return str(normalized.name)
+
+
+def _normalized_ref(ref: SqlAnalysisTableRef, dialect: str) -> SqlAnalysisTableRef:
+ return SqlAnalysisTableRef(
+ catalog=_normalize_identifier(ref.catalog, dialect),
+ db=_normalize_identifier(ref.db, dialect),
+ name=_normalize_identifier(ref.name, dialect) or ref.name,
+ )
+
+
+@dataclass(frozen=True)
+class _CatalogIndex:
+ by_full: dict[tuple[str | None, str | None, str], SqlAnalysisTableRef]
+ by_name: dict[str, list[SqlAnalysisTableRef]]
+
+
+def _catalog_index(
+ catalog: AnalyzeSqlCatalog | None, dialect: str
+) -> _CatalogIndex | None:
+ if catalog is None or not catalog.tables:
+ return None
+ by_full: dict[tuple[str | None, str | None, str], SqlAnalysisTableRef] = {}
+ by_name: dict[str, list[SqlAnalysisTableRef]] = {}
+ for table in catalog.tables:
+ ref = _normalized_ref(table, dialect)
+ key = (ref.catalog, ref.db, ref.name)
+ by_full[key] = ref
+ by_name.setdefault(ref.name, []).append(ref)
+ return _CatalogIndex(by_full=by_full, by_name=by_name)
+
+
+def _raw_table_ref(table: exp.Table, dialect: str) -> SqlAnalysisTableRef | None:
+ if not table.name:
+ return None
catalog = table.args.get("catalog")
db = table.args.get("db")
- if catalog is not None and getattr(catalog, "name", None):
- parts.append(str(catalog.name))
- if db is not None and getattr(db, "name", None):
- parts.append(str(db.name))
- if table.name:
- parts.append(str(table.name))
- return ".".join(parts)
+ return _normalized_ref(
+ SqlAnalysisTableRef(
+ catalog=str(catalog.name)
+ if catalog is not None and getattr(catalog, "name", None)
+ else None,
+ db=str(db.name) if db is not None and getattr(db, "name", None) else None,
+ name=str(table.name),
+ ),
+ dialect,
+ )
+
+
+def _resolve_table_refs(
+ raw: SqlAnalysisTableRef,
+ catalog: _CatalogIndex | None,
+) -> list[SqlAnalysisTableRef]:
+ if catalog is None:
+ return [raw]
+ exact = catalog.by_full.get((raw.catalog, raw.db, raw.name))
+ if exact is not None:
+ return [exact]
+ if raw.db is not None:
+ return [raw]
+ matches = catalog.by_name.get(raw.name, [])
+ if matches:
+ return matches
+ return [SqlAnalysisTableRef(catalog=None, db=None, name=raw.name)]
def _column_name(column: exp.Column) -> str:
@@ -146,33 +223,48 @@ def _columns_by_clause(tree: exp.Expression) -> dict[SqlAnalysisClause, list[str
return result
+def _table_refs(
+ tree: exp.Expression, dialect: str, catalog: _CatalogIndex | None
+) -> list[SqlAnalysisTableRef]:
+ normalized_tree = normalize_identifiers(tree, dialect=dialect)
+ qualified_tree = qualify_tables(normalized_tree, dialect=dialect)
+ cte_names = {cte.alias_or_name.lower() for cte in qualified_tree.find_all(exp.CTE)}
+ refs: list[SqlAnalysisTableRef] = []
+ seen: set[tuple[str | None, str | None, str]] = set()
+ for table in qualified_tree.find_all(exp.Table):
+ if table.name.lower() in cte_names:
+ continue
+ raw = _raw_table_ref(table, dialect)
+ if raw is None:
+ continue
+ for ref in _resolve_table_refs(raw, catalog):
+ key = (ref.catalog, ref.db, ref.name)
+ if key not in seen:
+ seen.add(key)
+ refs.append(ref)
+ return refs
+
+
def _analyze_one(
- item_id: str, sql: str, dialect: str
+ item_id: str, sql: str, dialect: str, catalog: _CatalogIndex | None
) -> tuple[str, AnalyzeSqlBatchResult]:
try:
tree = sqlglot.parse_one(sql, read=dialect)
except sqlglot.errors.SqlglotError as exc:
return item_id, AnalyzeSqlBatchResult(error=str(exc))
- cte_names = {cte.alias_or_name.lower() for cte in tree.find_all(exp.CTE)}
- table_refs = [
- table_ref
- for table_ref in (_table_ref(table) for table in tree.find_all(exp.Table))
- if table_ref and table_ref.split(".")[-1].lower() not in cte_names
- ]
-
return item_id, AnalyzeSqlBatchResult(
- tables_touched=_ordered_unique(table_refs),
+ tables_touched=_table_refs(tree, dialect, catalog),
columns_by_clause=_columns_by_clause(tree),
error=None,
)
def _analyze_payload(
- payload: tuple[str, str, str],
+ payload: tuple[str, str, str, _CatalogIndex | None],
) -> tuple[str, AnalyzeSqlBatchResult]:
- item_id, sql, dialect = payload
- return _analyze_one(item_id, sql, dialect)
+ item_id, sql, dialect, catalog = payload
+ return _analyze_one(item_id, sql, dialect, catalog)
def validate_read_only_sql_response(
@@ -222,7 +314,8 @@ def _worker_count(request: AnalyzeSqlBatchRequest) -> int:
def analyze_sql_batch_response(
request: AnalyzeSqlBatchRequest,
) -> AnalyzeSqlBatchResponse:
- payloads = [(item.id, item.sql, request.dialect) for item in request.items]
+ catalog = _catalog_index(request.catalog, request.dialect)
+ payloads = [(item.id, item.sql, request.dialect, catalog) for item in request.items]
if _worker_count(request) == 1:
analyzed = [_analyze_payload(payload) for payload in payloads]
else:
diff --git a/python/ktx-daemon/tests/test_app.py b/python/ktx-daemon/tests/test_app.py
index 9960daaf..2c3237ad 100644
--- a/python/ktx-daemon/tests/test_app.py
+++ b/python/ktx-daemon/tests/test_app.py
@@ -368,7 +368,9 @@ def test_sql_analyze_batch_endpoint_returns_per_item_results() -> None:
assert response.status_code == 200
body = response.json()
- assert body["results"]["orders"]["tables_touched"] == ["public.orders"]
+ assert body["results"]["orders"]["tables_touched"] == [
+ {"catalog": None, "db": "public", "name": "orders"}
+ ]
assert body["results"]["orders"]["columns_by_clause"] == {
"select": ["status"],
"where": ["created_at"],
diff --git a/python/ktx-daemon/tests/test_sql_analysis.py b/python/ktx-daemon/tests/test_sql_analysis.py
index 855d16fd..2fb3970a 100644
--- a/python/ktx-daemon/tests/test_sql_analysis.py
+++ b/python/ktx-daemon/tests/test_sql_analysis.py
@@ -32,7 +32,10 @@ def test_analyze_sql_batch_extracts_tables_and_clause_columns() -> None:
result = response.results["orders_by_customer"]
assert result.error is None
- assert result.tables_touched == ["public.orders", "public.customers"]
+ assert [item.model_dump() for item in result.tables_touched] == [
+ {"catalog": None, "db": "public", "name": "orders"},
+ {"catalog": None, "db": "public", "name": "customers"},
+ ]
assert result.columns_by_clause == {
"select": ["status"],
"where": ["created_at"],
@@ -56,6 +59,114 @@ def test_analyze_sql_batch_returns_per_item_parse_errors() -> None:
assert result.error is not None
+def test_analyze_sql_batch_qualifies_bare_table_from_catalog() -> None:
+ response = analyze_sql_batch_response(
+ AnalyzeSqlBatchRequest(
+ dialect="postgres",
+ catalog={
+ "tables": [
+ {
+ "catalog": None,
+ "db": "orbit_raw",
+ "name": "accounts",
+ "columns": ["id"],
+ },
+ {
+ "catalog": None,
+ "db": "orbit_analytics",
+ "name": "orders",
+ "columns": ["id"],
+ },
+ ]
+ },
+ items=[AnalyzeSqlBatchItem(id="bare", sql="select id from accounts")],
+ max_workers=1,
+ )
+ )
+
+ assert [item.model_dump() for item in response.results["bare"].tables_touched] == [
+ {"catalog": None, "db": "orbit_raw", "name": "accounts"}
+ ]
+
+
+def test_analyze_sql_batch_returns_all_ambiguous_modeled_matches() -> None:
+ response = analyze_sql_batch_response(
+ AnalyzeSqlBatchRequest(
+ dialect="postgres",
+ catalog={
+ "tables": [
+ {
+ "catalog": None,
+ "db": "orbit_raw",
+ "name": "events",
+ "columns": ["id"],
+ },
+ {
+ "catalog": None,
+ "db": "orbit_analytics",
+ "name": "events",
+ "columns": ["id"],
+ },
+ ]
+ },
+ items=[AnalyzeSqlBatchItem(id="ambiguous", sql="select id from events")],
+ max_workers=1,
+ )
+ )
+
+ assert [
+ item.model_dump() for item in response.results["ambiguous"].tables_touched
+ ] == [
+ {"catalog": None, "db": "orbit_raw", "name": "events"},
+ {"catalog": None, "db": "orbit_analytics", "name": "events"},
+ ]
+
+
+def test_analyze_sql_batch_leaves_unresolved_bare_refs_unqualified() -> None:
+ response = analyze_sql_batch_response(
+ AnalyzeSqlBatchRequest(
+ dialect="postgres",
+ catalog={
+ "tables": [{"catalog": None, "db": "orbit_raw", "name": "accounts"}]
+ },
+ items=[AnalyzeSqlBatchItem(id="missing", sql="select * from invoices")],
+ max_workers=1,
+ )
+ )
+
+ assert [
+ item.model_dump() for item in response.results["missing"].tables_touched
+ ] == [{"catalog": None, "db": None, "name": "invoices"}]
+
+
+def test_analyze_sql_batch_returns_bigquery_project_dataset_table_refs() -> None:
+ response = analyze_sql_batch_response(
+ AnalyzeSqlBatchRequest(
+ dialect="bigquery",
+ catalog={
+ "tables": [
+ {
+ "catalog": "demo-project",
+ "db": "orbit_analytics",
+ "name": "orders",
+ }
+ ]
+ },
+ items=[
+ AnalyzeSqlBatchItem(
+ id="bq",
+ sql="select * from `demo-project.orbit_analytics.orders`",
+ )
+ ],
+ max_workers=1,
+ )
+ )
+
+ assert [item.model_dump() for item in response.results["bq"].tables_touched] == [
+ {"catalog": "demo-project", "db": "orbit_analytics", "name": "orders"}
+ ]
+
+
def test_columns_from_nodes_ignores_non_expression_clause_values() -> None:
assert _columns_from_nodes([True, False, None]) == []
From 7ba948a13524388a89ef54fd082eff1aaaa9bb76 Mon Sep 17 00:00:00 2001
From: semantic-release-bot
Date: Wed, 3 Jun 2026 21:50:59 +0000
Subject: [PATCH 14/49] chore(release): 0.9.0 [skip ci]
## [0.9.0](https://github.com/Kaelio/ktx/compare/v0.8.0...v0.9.0) (2026-06-03)
### Features
* add codex llm backend for ktx runtime work ([#253](https://github.com/Kaelio/ktx/issues/253)) ([494618a](https://github.com/Kaelio/ktx/commit/494618ab142505bd988156d867be047e3affc4c3))
* **cli:** consistent connection setup recovery and build-time gate ([#257](https://github.com/Kaelio/ktx/issues/257)) ([ce1516b](https://github.com/Kaelio/ktx/commit/ce1516b357807874902d189d1d163755634083e8))
* **cli:** guide next action at end of ktx setup, not reruns ([#256](https://github.com/Kaelio/ktx/issues/256)) ([45aa95d](https://github.com/Kaelio/ktx/commit/45aa95d2cc121267bbbc8c184402a19573956dd4))
* **cli:** stream plain ktx ingest progress to stderr (KLO-726) ([#251](https://github.com/Kaelio/ktx/issues/251)) ([13774bf](https://github.com/Kaelio/ktx/commit/13774bfcef1622a83e29f27042bde1bcdd97beb2))
* **query-history:** scope mining to modeled schemas by default ([#258](https://github.com/Kaelio/ktx/issues/258)) ([e70ae1e](https://github.com/Kaelio/ktx/commit/e70ae1e63bcd7168ade90b8998a06b561ce36cf2))
* **telemetry:** include error details for failures ([#254](https://github.com/Kaelio/ktx/issues/254)) ([6da8c34](https://github.com/Kaelio/ktx/commit/6da8c3452a97bfcbeefd8bbcc3379d4d41b4dc9f))
### Bug Fixes
* **ingest:** recover textual-conflict gate failures; fix query-history adapter ([#255](https://github.com/Kaelio/ktx/issues/255)) ([f5dea9a](https://github.com/Kaelio/ktx/commit/f5dea9a0891305e7c4d90b0156638681fe75c1dc))
### Other Changes
* refresh star history chart [skip ci] ([9d3a0b7](https://github.com/Kaelio/ktx/commit/9d3a0b751df68c19df8007c4dec4c891f73246b0))
* refresh star history chart [skip ci] ([74c6076](https://github.com/Kaelio/ktx/commit/74c6076b72d0f79d8e7bfa8ef31550de39a36d00))
* refresh star history chart [skip ci] ([d01abe6](https://github.com/Kaelio/ktx/commit/d01abe6f3c8330dbdcf674ef8891e2b2118ac192))
* revert repo references to Kaelio/ktx and remove rename-resilience ([#252](https://github.com/Kaelio/ktx/issues/252)) ([41e20c9](https://github.com/Kaelio/ktx/commit/41e20c9ce7c4dcfc848073d72ae7c4ea766506fc)), closes [#250](https://github.com/Kaelio/ktx/issues/250) [#250](https://github.com/Kaelio/ktx/issues/250)
---
package.json | 2 +-
packages/cli/package.json | 2 +-
python/ktx-daemon/pyproject.toml | 2 +-
python/ktx-sl/pyproject.toml | 2 +-
release-policy.json | 2 +-
5 files changed, 5 insertions(+), 5 deletions(-)
diff --git a/package.json b/package.json
index a9590d70..e7714634 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "ktx-workspace",
- "version": "0.8.0",
+ "version": "0.9.0",
"description": "Workspace root for ktx packages",
"private": true,
"type": "module",
diff --git a/packages/cli/package.json b/packages/cli/package.json
index 9d3af54c..939a8b9c 100644
--- a/packages/cli/package.json
+++ b/packages/cli/package.json
@@ -1,6 +1,6 @@
{
"name": "@kaelio/ktx",
- "version": "0.8.0",
+ "version": "0.9.0",
"description": "Standalone ktx context layer for data agents",
"type": "module",
"engines": {
diff --git a/python/ktx-daemon/pyproject.toml b/python/ktx-daemon/pyproject.toml
index 97f2c15a..0fcf8e88 100644
--- a/python/ktx-daemon/pyproject.toml
+++ b/python/ktx-daemon/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "ktx-daemon"
-version = "0.8.0"
+version = "0.9.0"
description = "Portable compute package for KTX semantic-layer operations"
readme = "README.md"
requires-python = ">=3.13"
diff --git a/python/ktx-sl/pyproject.toml b/python/ktx-sl/pyproject.toml
index 01eb184d..aaf65265 100644
--- a/python/ktx-sl/pyproject.toml
+++ b/python/ktx-sl/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "ktx-sl"
-version = "0.8.0"
+version = "0.9.0"
description = "Agent-first semantic layer engine with aggregate locality"
readme = "README.md"
requires-python = ">=3.13"
diff --git a/release-policy.json b/release-policy.json
index 7cb55839..33774673 100644
--- a/release-policy.json
+++ b/release-policy.json
@@ -19,7 +19,7 @@
},
"publishedPackageSmoke": {
"packageName": "@kaelio/ktx",
- "version": "0.8.0",
+ "version": "0.9.0",
"registry": null
},
"runtimeInstaller": {
From 8eb1cd3e7947f7803d05dc8f204afcaf6d8eb92b Mon Sep 17 00:00:00 2001
From: "github-actions[bot]"
<41898282+github-actions[bot]@users.noreply.github.com>
Date: Thu, 4 Jun 2026 07:45:37 +0000
Subject: [PATCH 15/49] chore: refresh star history chart [skip ci]
---
assets/star-history.svg | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/assets/star-history.svg b/assets/star-history.svg
index 23016f3e..98de4631 100644
--- a/assets/star-history.svg
+++ b/assets/star-history.svg
@@ -1 +1 @@
-star-history.com May 17 May 24 May 31 200 400 600 800 kaelio/ktx Star History Date GitHub Stars
+star-history.com May 17 May 24 May 31 200 400 600 800 kaelio/ktx Star History Date GitHub Stars
From c2beaf7d5569197f7267697f5d65ac3dd1c60d9f Mon Sep 17 00:00:00 2001
From: Andrey Avtomonov
Date: Thu, 4 Jun 2026 14:11:08 +0200
Subject: [PATCH 16/49] feat(setup): wizard prompt tweaks and quieter
query-history filter output (#259)
Setup wizard flow tweaks:
- Add a reveal-tail password prompt (reveal-password-prompt.ts) that unmasks
the last few characters of a typed/pasted secret, and wire it into the setup
prompt adapter in place of clack's password(); adds the @clack/core dep.
- Reorder wizard select options: surface "Paste a key" before the
environment-variable option across embeddings/models/sources, promote
Metabase/Notion in the source list, put Git URL before Local path, reorder
the Notion crawl-mode choices, and relabel the sources "Done" action.
Query-history filter picker output:
- Collapse the per-template parse-failure lines into a single count in the
setup output and route the full template-id list to --debug stderr.
- Model parse failures as a structured parseFailedTemplateIds field instead of
warning strings.
- Add a privacy-safe query_history_filter_completed telemetry event
(counts/enums only), mirrored into the Python daemon schema.
---
packages/cli/package.json | 1 +
packages/cli/src/commands/setup-commands.ts | 3 +
.../query-history-filter-picker.ts | 9 +-
packages/cli/src/reveal-password-prompt.ts | 93 +++++++++++++++++++
packages/cli/src/setup-databases.ts | 45 +++++++--
packages/cli/src/setup-embeddings.ts | 2 +-
packages/cli/src/setup-models.ts | 2 +-
packages/cli/src/setup-prompts.ts | 4 +-
packages/cli/src/setup-sources.ts | 14 +--
packages/cli/src/setup.ts | 2 +
packages/cli/src/telemetry/events.schema.json | 82 ++++++++++++++++
packages/cli/src/telemetry/events.ts | 16 ++++
.../query-history-filter-picker.test.ts | 48 ++++++++++
.../cli/test/reveal-password-prompt.test.ts | 40 ++++++++
packages/cli/test/setup-databases.test.ts | 51 ++++++++++
packages/cli/test/setup-prompts.test.ts | 13 ++-
packages/cli/test/setup-sources.test.ts | 12 +--
packages/cli/test/telemetry/events.test.ts | 1 +
pnpm-lock.yaml | 3 +
.../ktx_daemon/telemetry/events.schema.json | 82 ++++++++++++++++
.../tests/test_telemetry_schema_sync.py | 1 +
uv.lock | 4 +-
22 files changed, 494 insertions(+), 34 deletions(-)
create mode 100644 packages/cli/src/reveal-password-prompt.ts
create mode 100644 packages/cli/test/reveal-password-prompt.test.ts
diff --git a/packages/cli/package.json b/packages/cli/package.json
index 939a8b9c..ba769d58 100644
--- a/packages/cli/package.json
+++ b/packages/cli/package.json
@@ -47,6 +47,7 @@
"@ai-sdk/devtools": "0.0.18",
"@ai-sdk/google-vertex": "^4.0.134",
"@anthropic-ai/claude-agent-sdk": "0.3.146",
+ "@clack/core": "1.3.1",
"@clack/prompts": "1.4.0",
"@clickhouse/client": "^1.18.5",
"@commander-js/extra-typings": "14.0.0",
diff --git a/packages/cli/src/commands/setup-commands.ts b/packages/cli/src/commands/setup-commands.ts
index 1619a80a..0302e9ed 100644
--- a/packages/cli/src/commands/setup-commands.ts
+++ b/packages/cli/src/commands/setup-commands.ts
@@ -406,6 +406,8 @@ export function registerSetupCommands(program: Command, context: KtxCliCommandCo
}
const resolvedAgentScope = options.local ? 'local' : options.global ? 'global' : 'project';
+ const debugEnabled =
+ ((command.optsWithGlobals ? command.optsWithGlobals() : command.opts()) as { debug?: unknown }).debug === true;
await runSetupArgs(context, {
command: 'run',
projectDir: resolveCommandProjectDir(command),
@@ -415,6 +417,7 @@ export function registerSetupCommands(program: Command, context: KtxCliCommandCo
agentScope: resolvedAgentScope,
skipAgents: options.skipAgents === true,
inputMode: options.input === false ? 'disabled' : 'auto',
+ ...(debugEnabled ? { debug: true } : {}),
yes: options.yes === true,
cliVersion: context.packageInfo.version,
...(options.llmBackend ? { llmBackend: options.llmBackend } : {}),
diff --git a/packages/cli/src/context/ingest/adapters/historic-sql/query-history-filter-picker.ts b/packages/cli/src/context/ingest/adapters/historic-sql/query-history-filter-picker.ts
index bb296513..3f77900d 100644
--- a/packages/cli/src/context/ingest/adapters/historic-sql/query-history-filter-picker.ts
+++ b/packages/cli/src/context/ingest/adapters/historic-sql/query-history-filter-picker.ts
@@ -23,6 +23,7 @@ export interface QueryHistoryFilterProposal {
consideredRoleCount: number;
skipped: { reason: 'no-llm' | 'no-daemon' | 'no-in-scope-history' | 'user-block-present' } | null;
warnings: string[];
+ parseFailedTemplateIds: string[];
}
export interface ProposeQueryHistoryServiceAccountFiltersInput {
@@ -74,7 +75,7 @@ const queryHistoryFilterAdjudicationSchema = z.object({
type QueryHistoryFilterAdjudication = z.infer;
function emptyProposal(skipped: QueryHistoryFilterProposal['skipped'], warnings: string[] = []): QueryHistoryFilterProposal {
- return { excludedRoles: [], consideredRoleCount: 0, skipped, warnings };
+ return { excludedRoles: [], consideredRoleCount: 0, skipped, warnings, parseFailedTemplateIds: [] };
}
function displayTableRef(ref: KtxTableRef): string {
@@ -180,6 +181,7 @@ export async function proposeQueryHistoryServiceAccountFilters(
const windowDays = 'windowDays' in config ? config.windowDays : 90;
const windowStart = new Date(now.getTime() - windowDays * 24 * 60 * 60 * 1000);
const warnings: string[] = [];
+ const parseFailedTemplateIds: string[] = [];
const snapshot: AggregatedTemplate[] = [];
try {
@@ -212,7 +214,7 @@ export async function proposeQueryHistoryServiceAccountFilters(
for (const template of snapshot) {
const parsed = analysis.get(template.templateId);
if (!parsed || parsed.error) {
- warnings.push(`query_history_filter_picker_parse_failed:${template.templateId}`);
+ parseFailedTemplateIds.push(template.templateId);
continue;
}
const tablesTouched = [...new Map(parsed.tablesTouched.map((ref) => [tableRefKey(ref), ref])).values()]
@@ -236,6 +238,7 @@ export async function proposeQueryHistoryServiceAccountFilters(
consideredRoleCount: records.length,
skipped: { reason: 'no-in-scope-history' },
warnings,
+ parseFailedTemplateIds,
};
}
@@ -256,6 +259,7 @@ export async function proposeQueryHistoryServiceAccountFilters(
...warnings,
`query_history_filter_picker_llm_failed:${error instanceof Error ? error.message : String(error)}`,
],
+ parseFailedTemplateIds,
};
}
@@ -274,5 +278,6 @@ export async function proposeQueryHistoryServiceAccountFilters(
consideredRoleCount: records.length,
skipped: input.userServiceAccountsPresent ? { reason: 'user-block-present' } : null,
warnings,
+ parseFailedTemplateIds,
};
}
diff --git a/packages/cli/src/reveal-password-prompt.ts b/packages/cli/src/reveal-password-prompt.ts
new file mode 100644
index 00000000..3fe3ed66
--- /dev/null
+++ b/packages/cli/src/reveal-password-prompt.ts
@@ -0,0 +1,93 @@
+import { styleText } from 'node:util';
+import { PasswordPrompt, type PasswordOptions } from '@clack/core';
+import { S_BAR, S_BAR_END, S_PASSWORD_MASK, settings, symbol } from '@clack/prompts';
+
+// How many trailing characters of a pasted secret to leave visible so the user
+// can confirm what landed (e.g. `••••••a1b2`). Kept small on purpose.
+const REVEAL_TAIL_COUNT = 4;
+
+/**
+ * Mask every character of `userInput` except the last `tail`, but only reveal the
+ * tail once the secret is long enough that the hidden portion still dominates
+ * (`length > tail * 2`). Short secrets stay fully masked so we never expose most
+ * of a small value. The returned string keeps the same code-unit length as the
+ * input so clack's cursor slicing in `userInputWithCursor` stays aligned.
+ *
+ * @internal
+ */
+export function maskRevealingTail(userInput: string, maskChar: string, tail: number): string {
+ const revealLength = userInput.length > tail * 2 ? tail : 0;
+ const hiddenLength = userInput.length - revealLength;
+ return maskChar.repeat(hiddenLength) + userInput.slice(hiddenLength);
+}
+
+class RevealTailPasswordPrompt extends PasswordPrompt {
+ readonly #maskChar: string;
+ readonly #tail: number;
+
+ constructor(options: PasswordOptions & { tail: number }) {
+ super(options);
+ this.#maskChar = options.mask ?? S_PASSWORD_MASK;
+ this.#tail = options.tail;
+ }
+
+ override get masked(): string {
+ return maskRevealingTail(this.userInput, this.#maskChar, this.#tail);
+ }
+}
+
+// Reproduces the @clack/prompts password frame (pinned to the installed version)
+// so this prompt is visually identical to every other setup prompt; the only
+// behavioral change is the tail-revealing `masked` getter above.
+function renderPasswordFrame(prompt: Omit, message: string): string {
+ const withGuide = settings.withGuide;
+ const title = `${withGuide ? `${styleText('gray', S_BAR)}\n` : ''}${symbol(prompt.state)} ${message}\n`;
+ const masked = prompt.masked;
+ switch (prompt.state) {
+ case 'error': {
+ const bar = withGuide ? `${styleText('yellow', S_BAR)} ` : '';
+ const end = withGuide ? `${styleText('yellow', S_BAR_END)} ` : '';
+ return `${title.trim()}\n${bar}${masked}\n${end}${styleText('yellow', prompt.error)}\n`;
+ }
+ case 'submit': {
+ const bar = withGuide ? `${styleText('gray', S_BAR)} ` : '';
+ return `${title}${bar}${masked ? styleText('dim', masked) : ''}`;
+ }
+ case 'cancel': {
+ const bar = withGuide ? `${styleText('gray', S_BAR)} ` : '';
+ const body = masked ? styleText(['strikethrough', 'dim'], masked) : '';
+ return `${title}${bar}${body}${masked && withGuide ? `\n${styleText('gray', S_BAR)}` : ''}`;
+ }
+ default: {
+ const bar = withGuide ? `${styleText('cyan', S_BAR)} ` : '';
+ const end = withGuide ? styleText('cyan', S_BAR_END) : '';
+ return `${title}${bar}${prompt.userInputWithCursor}\n${end}\n`;
+ }
+ }
+}
+
+export interface RevealPasswordOptions {
+ message: string;
+ mask?: string;
+ tail?: number;
+ validate?: PasswordOptions['validate'];
+ signal?: AbortSignal;
+}
+
+/**
+ * Drop-in replacement for clack's `password()` that reveals the last few
+ * characters of the entered value while typing. Resolves to the raw value or the
+ * clack cancel symbol, matching `password()`'s contract.
+ */
+export function revealPassword(options: RevealPasswordOptions): Promise {
+ const prompt = new RevealTailPasswordPrompt({
+ mask: options.mask ?? S_PASSWORD_MASK,
+ tail: options.tail ?? REVEAL_TAIL_COUNT,
+ validate: options.validate,
+ signal: options.signal,
+ render() {
+ return renderPasswordFrame(this, options.message);
+ },
+ });
+ return prompt.prompt() as Promise;
+}
diff --git a/packages/cli/src/setup-databases.ts b/packages/cli/src/setup-databases.ts
index 3cb6c5d2..002ead30 100644
--- a/packages/cli/src/setup-databases.ts
+++ b/packages/cli/src/setup-databases.ts
@@ -73,6 +73,7 @@ export type KtxSetupDatabaseDriver =
export interface KtxSetupDatabasesArgs {
projectDir: string;
inputMode: 'auto' | 'disabled';
+ debug?: boolean;
yes?: boolean;
cliVersion?: string;
runtimeInstallPolicy?: KtxManagedPythonInstallPolicy;
@@ -1626,7 +1627,12 @@ function hasServiceAccountsBlock(connection: KtxProjectConnectionConfig | undefi
return 'serviceAccounts' in filters;
}
-function printQueryHistoryFilterProposal(io: KtxCliIo, proposal: QueryHistoryFilterProposal): void {
+function printQueryHistoryFilterProposal(io: KtxCliIo, proposal: QueryHistoryFilterProposal, debug = false): void {
+ if (debug && proposal.parseFailedTemplateIds.length > 0) {
+ io.stderr.write(
+ `[debug] query-history filter picker could not parse ${proposal.parseFailedTemplateIds.length} template(s): ${proposal.parseFailedTemplateIds.join(', ')}\n`,
+ );
+ }
if (proposal.excludedRoles.length === 0) {
if (proposal.skipped?.reason === 'no-llm') {
io.stdout.write('│ Query-history filter picker skipped: no LLM is configured.\n');
@@ -1635,6 +1641,12 @@ function printQueryHistoryFilterProposal(io: KtxCliIo, proposal: QueryHistoryFil
} else if (proposal.skipped?.reason === 'no-in-scope-history') {
io.stdout.write('│ Query-history filter picker found no in-scope service-account exclusions.\n');
}
+ if (proposal.parseFailedTemplateIds.length > 0) {
+ const count = proposal.parseFailedTemplateIds.length;
+ io.stdout.write(
+ `│ Skipped ${count} query template${count === 1 ? '' : 's'} ktx could not parse (run with --debug to list them).\n`,
+ );
+ }
for (const warning of proposal.warnings) {
io.stdout.write(`│ ! ${warning}\n`);
}
@@ -1727,12 +1739,17 @@ async function maybeProposeQueryHistoryFilters(input: {
deps: input.deps,
});
if (!llmRuntime && !input.deps.queryHistoryFilterPicker) {
- printQueryHistoryFilterProposal(input.io, {
- excludedRoles: [],
- consideredRoleCount: 0,
- skipped: { reason: 'no-llm' },
- warnings: [],
- });
+ printQueryHistoryFilterProposal(
+ input.io,
+ {
+ excludedRoles: [],
+ consideredRoleCount: 0,
+ skipped: { reason: 'no-llm' },
+ warnings: [],
+ parseFailedTemplateIds: [],
+ },
+ input.args.debug === true,
+ );
return;
}
@@ -1773,7 +1790,19 @@ async function maybeProposeQueryHistoryFilters(input: {
userServiceAccountsPresent,
});
- printQueryHistoryFilterProposal(input.io, proposal);
+ printQueryHistoryFilterProposal(input.io, proposal, input.args.debug === true);
+ await emitTelemetryEvent({
+ name: 'query_history_filter_completed',
+ projectDir: input.projectDir,
+ io: input.io,
+ fields: {
+ dialect,
+ consideredRoleCount: proposal.consideredRoleCount,
+ excludedRoleCount: proposal.excludedRoles.length,
+ parseFailedCount: proposal.parseFailedTemplateIds.length,
+ outcome: 'ok',
+ },
+ });
if (proposal.skipped?.reason === 'user-block-present') {
input.io.stdout.write('│ Existing query-history service-account filters left unchanged.\n');
return;
diff --git a/packages/cli/src/setup-embeddings.ts b/packages/cli/src/setup-embeddings.ts
index 8f49bcf1..5d02e3e4 100644
--- a/packages/cli/src/setup-embeddings.ts
+++ b/packages/cli/src/setup-embeddings.ts
@@ -222,8 +222,8 @@ async function chooseCredentialRef(
const choice = await prompts.select({
message: `How should KTX find your ${embeddingBackendDisplayName(backend)} embedding API key?`,
options: [
- { value: 'env', label: `Use ${defaultEnv} from the environment` },
{ value: 'paste', label: 'Paste a key and save it as a local secret file' },
+ { value: 'env', label: `Use ${defaultEnv} from the environment` },
{ value: 'back', label: 'Back' },
],
});
diff --git a/packages/cli/src/setup-models.ts b/packages/cli/src/setup-models.ts
index 8e8cf30b..e673cb99 100644
--- a/packages/cli/src/setup-models.ts
+++ b/packages/cli/src/setup-models.ts
@@ -470,8 +470,8 @@ async function chooseCredentialRef(
const choice = await prompts.select({
message: `How should KTX find your Anthropic API key?\n\n${ANTHROPIC_CREDENTIAL_PROMPT_CONTEXT}`,
options: [
- { value: 'env', label: 'Use ANTHROPIC_API_KEY from the environment' },
{ value: 'paste', label: 'Paste a key and save it as a local secret file' },
+ { value: 'env', label: 'Use ANTHROPIC_API_KEY from the environment' },
{ value: 'back', label: 'Back' },
],
});
diff --git a/packages/cli/src/setup-prompts.ts b/packages/cli/src/setup-prompts.ts
index 1609bd76..e508d8ff 100644
--- a/packages/cli/src/setup-prompts.ts
+++ b/packages/cli/src/setup-prompts.ts
@@ -9,12 +9,12 @@ import {
log,
multiselect,
note,
- password,
select,
text,
} from '@clack/prompts';
import type { KtxCliIo } from './cli-runtime.js';
import { withMenuOptionsSpacing, withTextInputNavigation } from './prompt-navigation.js';
+import { revealPassword } from './reveal-password-prompt.js';
import { withSetupInterruptConfirmation } from './setup-interrupt.js';
export interface KtxSetupPromptOption {
@@ -189,7 +189,7 @@ export function createKtxSetupPromptAdapter(options: KtxSetupPromptAdapterOption
},
async password(promptOptions) {
const value = await withSetupInterruptConfirmation(() =>
- password({ ...promptOptions, message: withTextInputNavigation(promptOptions.message) }),
+ revealPassword({ ...promptOptions, message: withTextInputNavigation(promptOptions.message) }),
);
return isCancel(value) ? undefined : String(value);
},
diff --git a/packages/cli/src/setup-sources.ts b/packages/cli/src/setup-sources.ts
index 0a66c3a7..25552fbf 100644
--- a/packages/cli/src/setup-sources.ts
+++ b/packages/cli/src/setup-sources.ts
@@ -119,11 +119,11 @@ export interface KtxSetupSourcesDeps {
const SOURCE_OPTIONS: Array<{ value: KtxSetupSourceType; label: string }> = [
{ value: 'dbt', label: 'dbt' },
- { value: 'metricflow', label: 'MetricFlow' },
{ value: 'metabase', label: 'Metabase' },
+ { value: 'notion', label: 'Notion' },
+ { value: 'metricflow', label: 'MetricFlow' },
{ value: 'looker', label: 'Looker' },
{ value: 'lookml', label: 'LookML' },
- { value: 'notion', label: 'Notion' },
];
const SOURCE_LABELS = Object.fromEntries(SOURCE_OPTIONS.map((option) => [option.value, option.label])) as Record<
@@ -269,8 +269,8 @@ async function chooseSourceCredentialRef(input: {
message: `How should KTX find your ${input.label}?`,
options: [
...(input.existingRef ? [{ value: 'keep', label: 'Keep existing credential' }] : []),
- { value: 'env', label: `Use ${input.envName} from the environment` },
{ value: 'paste', label: 'Paste a key and save it as a local secret file' },
+ { value: 'env', label: `Use ${input.envName} from the environment` },
{ value: 'back', label: 'Back' },
],
});
@@ -307,8 +307,8 @@ async function chooseGitAuthCredentialRef(input: {
message: `${label} repo requires authentication.`,
options: [
...(input.existingRef ? [{ value: 'keep', label: 'Keep existing credential' }] : []),
- { value: 'env', label: 'Use GITHUB_TOKEN from the environment' },
{ value: 'paste', label: 'Paste a token and save it as a local secret file' },
+ { value: 'env', label: 'Use GITHUB_TOKEN from the environment' },
{ value: 'skip', label: 'Skip — try without authentication' },
{ value: 'back', label: 'Back' },
],
@@ -1063,8 +1063,8 @@ async function promptForInteractiveSource(
const selectedLocation = await prompts.select({
message: `${source} source location`,
options: [
- { value: 'path', label: 'Local path' },
{ value: 'git', label: 'Git URL' },
+ { value: 'path', label: 'Local path' },
{ value: 'back', label: 'Back' },
],
});
@@ -1343,8 +1343,8 @@ async function promptForInteractiveSource(
const crawlMode = await prompts.select({
message: 'Which Notion pages should KTX ingest?',
options: [
- { value: 'selected_roots', label: 'Specific pages and their subpages (choose them in a picker)' },
{ value: 'all_accessible', label: 'All pages the integration can access' },
+ { value: 'selected_roots', label: 'Specific pages and their subpages (choose them in a picker)' },
{ value: 'back', label: 'Back' },
],
});
@@ -2064,7 +2064,7 @@ export async function runKtxSetupSourcesStep(
const addMore = await prompts.select({
message: `${readyConnectionIds.length} context source${readyConnectionIds.length > 1 ? 's' : ''} configured (${readyConnectionIds.join(', ')}). Add another?`,
options: [
- { value: 'done', label: 'Done — continue to context build' },
+ { value: 'done', label: 'Done adding context sources' },
{ value: 'edit', label: 'Edit an existing context source' },
{ value: 'add', label: 'Add another context source' },
],
diff --git a/packages/cli/src/setup.ts b/packages/cli/src/setup.ts
index a991367e..fc45abb3 100644
--- a/packages/cli/src/setup.ts
+++ b/packages/cli/src/setup.ts
@@ -80,6 +80,7 @@ export type KtxSetupArgs =
agentScope?: KtxAgentScope;
skipAgents?: boolean;
inputMode: 'auto' | 'disabled';
+ debug?: boolean;
yes: boolean;
cliVersion: string;
llmBackend?: KtxSetupLlmBackend;
@@ -735,6 +736,7 @@ async function runKtxSetupInner(args: KtxSetupArgs, io: KtxCliIo, deps: KtxSetup
{
projectDir: projectResult.projectDir,
inputMode: args.inputMode,
+ ...(args.debug !== undefined ? { debug: args.debug } : {}),
yes: args.yes,
cliVersion: args.cliVersion,
runtimeInstallPolicy: setupRuntimeInstallPolicy(args),
diff --git a/packages/cli/src/telemetry/events.schema.json b/packages/cli/src/telemetry/events.schema.json
index a75f92f1..c6c3d6f8 100644
--- a/packages/cli/src/telemetry/events.schema.json
+++ b/packages/cli/src/telemetry/events.schema.json
@@ -206,6 +206,17 @@
"errorClass",
"durationMs"
]
+ },
+ {
+ "name": "query_history_filter_completed",
+ "description": "Emitted after the setup query-history service-account filter picker runs.",
+ "fields": [
+ "dialect",
+ "consideredRoleCount",
+ "excludedRoleCount",
+ "parseFailedCount",
+ "outcome"
+ ]
}
],
"$defs": {
@@ -1434,6 +1445,77 @@
"durationMs"
],
"additionalProperties": false
+ },
+ "query_history_filter_completed": {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "type": "object",
+ "properties": {
+ "cliVersion": {
+ "type": "string"
+ },
+ "nodeVersion": {
+ "type": "string"
+ },
+ "osPlatform": {
+ "type": "string"
+ },
+ "osRelease": {
+ "type": "string"
+ },
+ "arch": {
+ "type": "string"
+ },
+ "runtime": {
+ "type": "string",
+ "enum": [
+ "node",
+ "daemon-py"
+ ]
+ },
+ "isCi": {
+ "type": "boolean"
+ },
+ "dialect": {
+ "type": "string"
+ },
+ "consideredRoleCount": {
+ "type": "integer",
+ "minimum": 0,
+ "maximum": 9007199254740991
+ },
+ "excludedRoleCount": {
+ "type": "integer",
+ "minimum": 0,
+ "maximum": 9007199254740991
+ },
+ "parseFailedCount": {
+ "type": "integer",
+ "minimum": 0,
+ "maximum": 9007199254740991
+ },
+ "outcome": {
+ "type": "string",
+ "enum": [
+ "ok",
+ "error"
+ ]
+ }
+ },
+ "required": [
+ "cliVersion",
+ "nodeVersion",
+ "osPlatform",
+ "osRelease",
+ "arch",
+ "runtime",
+ "isCi",
+ "dialect",
+ "consideredRoleCount",
+ "excludedRoleCount",
+ "parseFailedCount",
+ "outcome"
+ ],
+ "additionalProperties": false
}
}
}
diff --git a/packages/cli/src/telemetry/events.ts b/packages/cli/src/telemetry/events.ts
index c4fc2e6f..cf650492 100644
--- a/packages/cli/src/telemetry/events.ts
+++ b/packages/cli/src/telemetry/events.ts
@@ -206,6 +206,16 @@ const sqlGenCompletedSchema = telemetryCommonEnvelopeSchema
})
.strict();
+const queryHistoryFilterCompletedSchema = telemetryCommonEnvelopeSchema
+ .extend({
+ dialect: z.string(),
+ consideredRoleCount: z.number().int().nonnegative(),
+ excludedRoleCount: z.number().int().nonnegative(),
+ parseFailedCount: z.number().int().nonnegative(),
+ outcome: outcomeSchema,
+ })
+ .strict();
+
/** @internal */
export const telemetryEventSchemas = {
install_first_run: installFirstRunSchema,
@@ -225,6 +235,7 @@ export const telemetryEventSchemas = {
daemon_stopped: daemonStoppedSchema,
sl_plan_completed: slPlanCompletedSchema,
sql_gen_completed: sqlGenCompletedSchema,
+ query_history_filter_completed: queryHistoryFilterCompletedSchema,
} as const;
/** @internal */
@@ -360,6 +371,11 @@ export const telemetryEventCatalog = [
description: 'Emitted after daemon SQL generation completes.',
fields: ['outcome', 'dialect', 'errorClass', 'durationMs'],
},
+ {
+ name: 'query_history_filter_completed',
+ description: 'Emitted after the setup query-history service-account filter picker runs.',
+ fields: ['dialect', 'consideredRoleCount', 'excludedRoleCount', 'parseFailedCount', 'outcome'],
+ },
] as const;
export type TelemetryEventName = keyof typeof telemetryEventSchemas;
diff --git a/packages/cli/test/context/ingest/adapters/historic-sql/query-history-filter-picker.test.ts b/packages/cli/test/context/ingest/adapters/historic-sql/query-history-filter-picker.test.ts
index 4c295092..5c9e2e60 100644
--- a/packages/cli/test/context/ingest/adapters/historic-sql/query-history-filter-picker.test.ts
+++ b/packages/cli/test/context/ingest/adapters/historic-sql/query-history-filter-picker.test.ts
@@ -64,6 +64,27 @@ function sqlAnalysis(tablesById: Record>,
+ errorIds: string[],
+): SqlAnalysisPort {
+ const errors = new Set(errorIds);
+ return {
+ analyzeForFingerprint: vi.fn(),
+ analyzeBatch: vi.fn(async (items: SqlAnalysisBatchItem[]): Promise