diff --git a/.github/styles/config/vocabularies/VGI/accept.txt b/.github/styles/config/vocabularies/VGI/accept.txt new file mode 100644 index 0000000..555b9da --- /dev/null +++ b/.github/styles/config/vocabularies/VGI/accept.txt @@ -0,0 +1,41 @@ +VGI +vgi +[Vv]gi-python +[Vv]gi-rpc +DuckDB +Arrow +[Aa]pache Arrow +Haybarn +pyarrow +PyArrow +RecordBatch +RecordBatches +mkdocstrings +MkDocs +uv +uvx +subprocess +scalar +[Aa]ggregations? +[Dd]eserialize +[Ss]erializable +Diátaxis +ATTACH +classmethod +dataclass +runnable +bool +config +[Mm]etadata +namespace +namespaces +struct +async +stdin +stdout +optimizer +[Pp]ushdown +JWT +HTTP +OAuth +TTL diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 491b74f..471cea6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -65,6 +65,50 @@ jobs: run: uv run ty check vgi/ continue-on-error: true + docs: + name: Docs (build + examples + prose) + runs-on: ubuntu-latest + env: + DISABLE_MKDOCS_2_WARNING: "true" + steps: + - uses: actions/checkout@v5 + + - name: Install uv + uses: astral-sh/setup-uv@v8.2.0 + + - name: Set up Python 3.13 + run: uv python install 3.13 + + - name: Install dependencies + run: uv sync --all-extras --group docs + + - name: Install d2 + run: curl -fsSL https://d2lang.com/install.sh | sh -s -- + + - name: Build docs (strict) + run: uv run mkdocs build --strict + + - name: Test documentation examples + run: uv run pytest tests/test_documentation_examples.py tests/test_examples_workers.py -q + + # Prose lint. Runs on every PR as a signal. Kept non-blocking for now: + # the Google package + spelling check needs a vocabulary-tuning pass + # against a real run before it can gate without false positives. Flip + # fail_on_error to true (and drop continue-on-error) once the vocab in + # .github/styles/config/vocabularies/VGI/accept.txt is settled. + - name: Prose lint (Vale) + uses: errata-ai/vale-action@v2 + continue-on-error: true + with: + files: docs + fail_on_error: true + + - name: Link check (lychee) + uses: lycheeverse/lychee-action@v2 + with: + args: "--no-progress --offline docs/**/*.md *.md" + fail: true + s3-offload-localstack: name: S3 Offload Tests (LocalStack) runs-on: ubuntu-latest diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 0000000..5ef1d7e --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,46 @@ +name: Docs + +on: + push: + branches: [main] + paths: + - "docs/**" + - "mkdocs.yml" + - "vgi/**" + - "pyproject.toml" + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: pages + cancel-in-progress: true + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + + - name: Install uv + uses: astral-sh/setup-uv@v8.1.0 + + - name: Set up Python + run: uv python install 3.13 + + - name: Install dependencies + run: uv sync --group docs + + - name: Install d2 + run: curl -fsSL https://d2lang.com/install.sh | sh -s -- + + - name: Build docs + run: uv run mkdocs build --strict + + - name: Deploy to Cloudflare Pages + uses: cloudflare/wrangler-action@v3 + with: + apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} + accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + command: pages deploy site --project-name=vgi-python-docs --commit-dirty=true diff --git a/.gitignore b/.gitignore index 101c370..bc1c35f 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,11 @@ dist/ wheels/ *.egg-info +# MkDocs build output +/site/ +# MkDocs plugin caches (d2 diagram render cache) +/.cache/ + # Virtual environments .venv diff --git a/.vale.ini b/.vale.ini new file mode 100644 index 0000000..50f4422 --- /dev/null +++ b/.vale.ini @@ -0,0 +1,19 @@ +# Vale prose-lint config for vgi-python docs. +# Run locally with: vale docs/ (after `vale sync` to fetch the Google package) +StylesPath = .github/styles + +# Only fail on errors. The Google package emits many style suggestions/warnings; +# gating on those would be noisy, so the CI gate enforces error-level issues +# (Vale core checks + spelling against the VGI vocabulary) only. +MinAlertLevel = error + +Packages = Google + +Vocab = VGI + +[*.md] +BasedOnStyles = Vale, Google + +# Snippet directives and our auto-generated API pages aren't prose to lint. +[docs/api/*] +BasedOnStyles = diff --git a/DOCS_ACCEPTANCE_CRITERIA.md b/DOCS_ACCEPTANCE_CRITERIA.md new file mode 100644 index 0000000..0853b9f --- /dev/null +++ b/DOCS_ACCEPTANCE_CRITERIA.md @@ -0,0 +1,122 @@ +# VGI-Python Documentation — Acceptance Criteria (review-ready v1) + +> Status: DRAFT for senior DX-engineer review. Derived from a requirements interview. +> This document defines what "done and good" means for the documentation rework +> before the site goes live at `vgi-python.query.farm`. + +## North Star + +**A developer who has never used VGI can build and run a real worker fast.** +Everything on the site is optimized around that job-to-be-done; depth is available +but never blocks the fast path. + +## Target audience (mixed — serve all via progressive disclosure) + +The reader could be a Python developer new to DuckDB/Arrow, a DuckDB/SQL user newer +to Python, or someone fluent in both. Therefore: + +- The happy path is skimmable by experts (dense, copy-paste-ready). +- Newcomers are served by **progressive disclosure**: inline "New to Arrow? →" / + "New to DuckDB extensions? →" callouts and links, not walls of prerequisite text. +- We never assume knowledge silently; we either explain briefly or link out. + +## Information architecture — Diátaxis + +Top-level navigation is reorganized into the four Diátaxis modes (this directly +addresses the current "hard to orient" problem): + +1. **Tutorial** — one guided, end-to-end "build your first worker" path. +2. **How-to guides** — task-oriented recipes ("Add a table function", "Run over + HTTP with auth", "Persist aggregate state"). +3. **Concepts** — explanations: worker lifecycle (bind/init/process/finalize), + transports, the Arrow data model, catalogs & ATTACH, parallel workers. +4. **API Reference** — the existing auto-generated mkdocstrings pages. + +The current 11 hand-written guides are **re-homed** into How-to vs Concepts (not +left in a flat "Guides" bucket). + +## Scope + +### In scope for v1 (must be fully documented: tutorial coverage + how-to + runnable example) + +- **All four function patterns**: scalar, table, table-in-out, aggregate. +- **Catalogs / ATTACH model** — how functions are surfaced to DuckDB. +- **State storage** — `FunctionStorage` backends for stateful/aggregate functions. +- **Auth + HTTP transport** — running a worker over HTTP with bearer/JWT auth. +- **Filter pushdown & column statistics** — optimizer integration for table functions. + +### Out of scope for v1 (reference-only / deferred — must NOT block launch) + +- Transactor (transactional DB access) +- External storage / large-payload offload (S3/GCS) +- Observability (OpenTelemetry / Sentry) +- Sharding / meta-worker, cross-language client codegen, standalone secret service + +These remain available in the auto-generated API reference but get no tutorial/how-to +investment in v1. + +## Headline acceptance test (Time-To-First-Success) + +> **An unfamiliar developer, working unaided from the docs, has both a custom +> scalar function AND a custom table function callable from DuckDB within +> ≤20 minutes.** + +- "Callable from DuckDB" = `SELECT my_cat.my_scalar(col) FROM t` returns rows, and + `SELECT * FROM my_cat.my_table(args)` returns rows. +- Engine for the timed path: **Haybarn** (`uvx haybarn-cli`) as the primary happy + path; a stock-DuckDB variant (`INSTALL vgi FROM community; LOAD vgi;`) shown in a + callout/tab for portability. +- Every place a test participant gets stuck is logged and fixed before sign-off. + +## Per-page orientation standard (applies to every tutorial / how-to / concept page) + +Each page must contain: + +1. **Lead "what + who" line** — one sentence at the top: what this page is and who + it's for (reader self-orients in <10 s). +2. **Prerequisites stated** — explicit assumed knowledge, prior steps, and required + extras (`vgi-python[http]`, etc.), with links. +3. **At least one complete, runnable example** — no elisions; covered by the CI + example tests (see Quality Gates). +4. **"Next steps" links** — a closing section pointing to the logical next page(s); + no dead ends. + +## Example correctness bar + +- **100% of Python code blocks are copy-paste runnable and CI-tested** (e.g. via + `pytest-examples`, already a dev dependency). +- The tutorial worker is **built and queried end-to-end in an automated test**. +- A broken example fails the build. + +## Quality gates (all three required to sign off v1) + +1. **Fresh-dev usability test** — ≥1 developer unfamiliar with VGI completes the + headline acceptance test (scalar + table from DuckDB, ≤20 min, unaided). All + stumbling points resolved. +2. **Senior DX reviewer rubric** — named senior DX engineer(s) score the site + against a written checklist: orientation, scannability, completeness vs the + in-scope list, correctness, navigation, and the per-page standard above. All + must-fix items resolved before merge. +3. **Automated quality gates in CI**: + - `mkdocs build --strict` passes with zero warnings (no broken links / refs). + - All documentation examples execute successfully. + - Link check + prose/style lint pass. + +## Definition of Done (v1) + +- [ ] Diátaxis nav live (Tutorial / How-to / Concepts / API Reference); existing + guides re-homed. +- [ ] Guided tutorial takes a reader from zero → scalar + table function queried + from Haybarn, with the stock-DuckDB variant noted. +- [ ] How-to + runnable example exists for each in-scope topic (4 patterns + + catalogs + state storage + auth/HTTP + pushdown/stats). +- [ ] Concept pages cover lifecycle, transports, Arrow model, catalogs, parallelism. +- [ ] Every page meets the 4-point orientation standard. +- [ ] All examples runnable and CI-tested; tutorial validated end-to-end in CI. +- [ ] Out-of-scope topics confined to reference; not advertised as v1 guides. +- [ ] All three quality gates passed and signed off. + +## Open items to confirm with reviewers + +- Named senior DX reviewer(s) and the recruited fresh-dev test participant. +- Final wording/threshold of the prose-style lint (e.g. Vale ruleset), if adopted. diff --git a/DOCS_REVIEW_RUBRIC.md b/DOCS_REVIEW_RUBRIC.md new file mode 100644 index 0000000..e3408fa --- /dev/null +++ b/DOCS_REVIEW_RUBRIC.md @@ -0,0 +1,67 @@ +# vgi-python docs — senior DX review rubric + +> Status: review checklist for the review-ready v1 of the documentation +> (see `DOCS_ACCEPTANCE_CRITERIA.md`). A reviewer scores each item Pass / Fix / +> N/A. Every **Fix** must be resolved (or explicitly waived) before the site +> goes live at `vgi-python.query.farm`. + +Reviewer: ________________ Date: ________________ Commit: ________________ + +## 1. Orientation (the problem this rework targets) + +- [ ] The home page makes it obvious in <10 s what vgi-python is and where to start. +- [ ] Top-level nav clearly separates **Tutorial / How-to / Concepts / API Reference** + (Diátaxis); a newcomer can tell which to open for their need. +- [ ] Every tutorial/how-to/concept page opens with a **"what + who"** line. +- [ ] No page is a dead end — each ends with **"Next steps"** links. + +## 2. The fast path (job-to-be-done: ship a worker fast) + +- [ ] The tutorial gets a reader from zero → a **scalar + table** function callable from + DuckDB, and is realistically completable in **≤20 minutes**. +- [ ] The first step yields a working query quickly (scalar before table). +- [ ] Haybarn is the primary path; the stock-DuckDB variant is present and correct. +- [ ] Copy-paste works: the worker shown is complete and runnable as-is. + +## 3. Completeness vs. the in-scope list + +- [ ] All four function patterns are documented with a runnable example: scalar, table, + table-in-out, aggregate. +- [ ] Catalogs / ATTACH, state storage, auth + HTTP, and filter pushdown & stats each have a + how-to. +- [ ] Out-of-scope topics (transactor, external storage, observability, sharding/codegen/secret + service) are reference-only and not advertised as v1 guides. + +## 4. Correctness + +- [ ] Every code example is accurate and runs (CI: `test_documentation_examples.py` + + `test_examples_workers.py` green). +- [ ] SQL snippets use correct catalog/function names and match the worker shown. +- [ ] Conceptual claims (lifecycle phases, transports, Arrow semantics) are accurate. +- [ ] API reference renders for every in-scope module (CI: `mkdocs build --strict` green). + +## 5. Scannability & progressive disclosure + +- [ ] Pages use headings, tables, and short paragraphs; an expert can skim. +- [ ] Newcomer background is in collapsible callouts, not blocking the main flow. +- [ ] Prerequisites and required extras (`[http]`, `[oauth]`, …) are stated where needed. + +## 6. Navigation & polish + +- [ ] No broken links (CI: lychee + strict build green). +- [ ] Search returns sensible results for common terms (worker, scalar, aggregate, ATTACH). +- [ ] Light/dark themes, logo, and code-copy all work. + +## Automated gates (must be green at review time) + +- [ ] `mkdocs build --strict` — zero warnings +- [ ] `pytest tests/test_documentation_examples.py tests/test_examples_workers.py` +- [ ] lychee link-check +- [ ] Vale prose lint (advisory until vocab is tuned; note residual warnings) + +## Sign-off + +- [ ] All **Fix** items resolved or waived (waivers noted below). +- [ ] Fresh-dev usability test passed (see `DOCS_USABILITY_TEST.md`). + +Waivers / notes: diff --git a/DOCS_USABILITY_TEST.md b/DOCS_USABILITY_TEST.md new file mode 100644 index 0000000..cd3f095 --- /dev/null +++ b/DOCS_USABILITY_TEST.md @@ -0,0 +1,71 @@ +# vgi-python docs — fresh-dev usability test + +> The headline acceptance gate: an unfamiliar developer, working **only** from the +> docs, builds and runs a worker. This protocol scripts that test and captures +> what to fix. See `DOCS_ACCEPTANCE_CRITERIA.md` for the full criteria. + +## Goal (pass condition) + +> A developer **new to VGI**, working **unaided** from the published docs, reaches a custom +> **scalar AND table** function callable from DuckDB in **≤20 minutes**. + +"Callable from DuckDB" means both: + +- `SELECT greetings.greeting('Alice')` returns `Hello, Alice!`, and +- `SELECT * FROM greetings.greeting_series(3)` returns 3 rows. + +## Participant criteria + +- Comfortable writing Python; **has not** used VGI before. +- A mix is ideal across runs: at least one participant new to DuckDB extensions, and at least + one new to Apache Arrow (this is the "serve all" audience we're validating). +- Has Python 3.13+ and `uv` installed (or we install them first, off the clock). + +## Facilitator rules + +- **Do not help.** Point the participant at the docs home page and the timer; then observe in + silence. Answer only "I can't help with that — what would you try?" +- Record the clock at each milestone and **every** point of confusion verbatim. +- Capture the participant's words ("I don't know what a catalog is here") — those become doc fixes. + +## Script + +1. Start screen recording (or take notes) and the timer. +2. Give the participant only this: *"Using the vgi-python docs, build a worker that adds two + functions to DuckDB — one that greets a name, and one that generates a series of greetings — + and run both from SQL. Start at the docs home page."* +3. Observe until success or 30 minutes elapsed (let them run past 20 so we learn where the tail is). +4. Debrief: what was confusing, what was missing, what they expected to find and didn't. + +## Milestones (record the time reached) + +| Milestone | Time | Notes | +|---|---|---| +| Found the tutorial / starting point | | | +| Worker file written | | | +| SQL engine launched (Haybarn or DuckDB) | | | +| Worker attached (`ATTACH …`) | | | +| **Scalar query returned a result** | | | +| **Table query returned rows** | | | +| Total time to success (or DNF) | | | + +## Stumble log + +| # | Where (page / step) | What happened | Severity (block/slow/nit) | Fix | +|---|---|---|---|---| +| 1 | | | | | +| 2 | | | | | +| 3 | | | | | + +## Outcome + +- [ ] Passed (≤20 min, unaided, both functions) — participant: ____________ time: ______ +- [ ] Did not pass — root cause: ____________________________________________ + +**Every block/slow stumble must produce a doc fix (or an explicit waiver) before sign-off.** + +## Runs + +| Date | Participant background | Result | Time | Fixes filed | +|---|---|---|---|---| +| | | | | | diff --git a/README.md b/README.md index e3b9901..853480a 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,10 @@ Created by Query.Farm

+

+ 📖 Documentation: vgi-python.query.farm +

+ --- ## See It in Action @@ -112,7 +116,7 @@ VGI lets you extend DuckDB with Python functions that run in separate processes, | Traditional Extensions | VGI Workers | |----------------------|-------------| -| C/C++ compilation required | Any language but first Python and Typescript and Go | +| C/C++ compilation required | Any language with an Apache Arrow library | | Tied to DuckDB version | Version independent | | Complex build/release cycle | Ship a script or executable | | Runs in-process | Process isolation | diff --git a/docs/api/arguments.md b/docs/api/arguments.md new file mode 100644 index 0000000..664ad9b --- /dev/null +++ b/docs/api/arguments.md @@ -0,0 +1,17 @@ +# Arguments & Schema + +Function signatures are declared with the argument types below. The framework derives Arrow +schemas from these declarations and validates inputs before your function runs. See the +[Argument Serialization](../argument-serialization.md) guide for how these map to the wire. + +## Arguments + +::: vgi.arguments + +## Argument specifications + +::: vgi.argument_spec + +## Schema helpers + +::: vgi.schema_utils diff --git a/docs/api/auth.md b/docs/api/auth.md new file mode 100644 index 0000000..b96ca27 --- /dev/null +++ b/docs/api/auth.md @@ -0,0 +1,21 @@ +# Auth & Secrets + +HTTP workers authenticate requests via a pluggable callback that populates `CallContext.auth` with +an `AuthContext`. Bearer-token and JWT/JWKS authenticators ship in `vgi.auth`. Secrets (credentials) +flow through the secret protocol so workers never see raw values unless explicitly resolved. See the +[Authentication](../authentication.md) guide. + +`AuthContext` and `CallContext` are re-exported from `vgi-rpc`. The bearer/chain authenticators +require `pip install vgi-python[http]`; JWT authentication additionally requires `[oauth]`. + +## Auth + +::: vgi.auth + +## Secret protocol + +::: vgi.secret_protocol + +## Secret service + +::: vgi.secret_service diff --git a/docs/api/catalogs.md b/docs/api/catalogs.md new file mode 100644 index 0000000..6bb5c7c --- /dev/null +++ b/docs/api/catalogs.md @@ -0,0 +1,7 @@ +# Catalogs + +A worker can expose a database-like catalog — schemas, tables, views, macros, indexes, secrets, +and settings — so DuckDB can `ATTACH` it. See the [Catalog Interface](../catalog-interface.md) +guide for the conceptual overview. + +::: vgi.catalog diff --git a/docs/api/client.md b/docs/api/client.md new file mode 100644 index 0000000..d3e5f51 --- /dev/null +++ b/docs/api/client.md @@ -0,0 +1,6 @@ +# Client + +The Python `Client` spawns or connects to a worker, streams Arrow data to and from its functions, +and surfaces errors as `ClientError`. It is the pure-Python counterpart to the DuckDB extension. + +::: vgi.client diff --git a/docs/api/exceptions.md b/docs/api/exceptions.md new file mode 100644 index 0000000..93ef46b --- /dev/null +++ b/docs/api/exceptions.md @@ -0,0 +1,8 @@ +# Exceptions + +VGI raises typed exceptions for binding, catalog, schema, and execution errors. Several +validation errors also live alongside the features they guard (see +[Arguments](arguments.md#vgi.arguments.ArgumentValidationError) and +[Metadata](metadata.md)). + +::: vgi.exceptions diff --git a/docs/api/filters.md b/docs/api/filters.md new file mode 100644 index 0000000..59f9bc0 --- /dev/null +++ b/docs/api/filters.md @@ -0,0 +1,8 @@ +# Filter Pushdown + +Table functions can receive SQL `WHERE` predicates pushed down from DuckDB, letting the worker +prune data at the source. Filters arrive as a `PushdownFilters` tree of typed nodes; use +`deserialize_filters` to decode them. See the [Filter Pushdown](../filter-pushdown.md) guide for +the protocol and a worked example. + +::: vgi.table_filter_pushdown diff --git a/docs/api/functions.md b/docs/api/functions.md new file mode 100644 index 0000000..70dddc7 --- /dev/null +++ b/docs/api/functions.md @@ -0,0 +1,32 @@ +# Functions + +VGI exposes four function patterns. Pick the one that matches how your data flows: + +| Pattern | Shape | Base class | +|---|---|---| +| **Scalar** | 1 row in → 1 row out | [`ScalarFunction`](#vgi.scalar_function.ScalarFunction) / `ScalarFunctionGenerator` | +| **Table** | no input → rows out | [`TableFunctionGenerator`](#vgi.table_function.TableFunctionGenerator) | +| **Table-in-out** | rows in → rows out (streaming) | [`TableInOutFunction`](#vgi.table_in_out_function.TableInOutFunction) / `TableInOutGenerator` | +| **Aggregate** | grouped rows → one row per group | [`AggregateFunction`](#vgi.aggregate_function.AggregateFunction) | + +All four ultimately derive from the shared [`Function`](#vgi.function.Function) base. + +## Scalar functions + +::: vgi.scalar_function + +## Table functions + +::: vgi.table_function + +## Table-in-out functions + +::: vgi.table_in_out_function + +## Aggregate functions + +::: vgi.aggregate_function + +## Function base + +::: vgi.function diff --git a/docs/api/http.md b/docs/api/http.md new file mode 100644 index 0000000..3a8146f --- /dev/null +++ b/docs/api/http.md @@ -0,0 +1,6 @@ +# HTTP + +HTTP-transport utilities: a human-facing worker info page, demo blob storage for externalized +payloads, and request-size middleware. Requires `pip install vgi-python[http]`. + +::: vgi.http diff --git a/docs/api/index.md b/docs/api/index.md new file mode 100644 index 0000000..b894dfc --- /dev/null +++ b/docs/api/index.md @@ -0,0 +1,47 @@ +--- +description: "vgi-python API reference — functions, arguments, worker, client, catalogs, state storage, filter pushdown, auth, and observability." +--- + +# API Reference + +## Where to Start + +New to VGI? Follow this path: + +1. **Pick a function pattern** — `ScalarFunction`, `TableFunctionGenerator`, + `TableInOutFunction`, or `AggregateFunction` ([Functions](functions.md)) +2. **Declare arguments** — `Param`, `ConstParam`, `Returns`, `TableInput` ([Arguments & Schema](arguments.md)) +3. **Host them in a worker** — subclass `Worker`, then `vgi-serve` it over stdio or HTTP ([Worker & Serving](worker.md)) +4. **Connect from DuckDB or Python** — the DuckDB extension or the Python `Client` ([Client](client.md)) + +Everything else — catalogs, state storage, filter pushdown, auth, observability — is optional and +added incrementally. + +## Modules + +| Module | Description | Required? | +|---|---|---| +| [Functions](functions.md) | `ScalarFunction`, `TableFunctionGenerator`, `TableInOutFunction`, `AggregateFunction`, `Function` | Yes | +| [Arguments & Schema](arguments.md) | `Param`, `ConstParam`, `Returns`, `TableInput`, `ArgumentSpec`, `schema` | Yes | +| [Worker & Serving](worker.md) | `Worker`, the `vgi-serve` entry point | Yes | +| [Client](client.md) | `Client`, `ClientError`, catalog client helpers | If calling workers from Python | +| [Catalogs](catalogs.md) | `Catalog`, `Schema`, `Table`, `View`, `CatalogStorage` | If exposing a catalog | +| [State Storage](storage.md) | `FunctionStorage`, SQLite / Azure SQL / Cloudflare DO backends | If functions keep state | +| [Metadata & Protocol](metadata.md) | `ResolvedMetadata`, `FunctionExample`, `FunctionStability`, protocol types | For introspection | +| [Filter Pushdown](filters.md) | `PushdownFilters`, filter node types, `deserialize_filters` | If accepting pushed-down filters | +| [Auth & Secrets](auth.md) | `AuthContext`, `CallContext`, bearer/JWT authenticators, secret protocol | HTTP: `[http]`; JWT: `[oauth]` | +| [Observability](observability.md) | OpenTelemetry tracing, worker logging configuration | `[otel]` for tracing | +| [HTTP](http.md) | Worker page, blob storage, request-size middleware | `pip install vgi-python[http]` | +| [Transactor](transactor.md) | `TransactorClient`, `TransactorProtocol` | `pip install vgi-python[transactor]` | +| [Exceptions](exceptions.md) | VGI exception types | — | + +## Import Convention + +The most common symbols are re-exported from the top-level `vgi` package: + +```python +from vgi import ScalarFunction, TableInOutFunction, AggregateFunction, Param, Returns, Worker +``` + +Subpackages (`vgi.catalog`, `vgi.client`, `vgi.http`, `vgi.transactor`) are imported explicitly. +Optional modules require their corresponding extras to be installed. diff --git a/docs/api/metadata.md b/docs/api/metadata.md new file mode 100644 index 0000000..a77a10b --- /dev/null +++ b/docs/api/metadata.md @@ -0,0 +1,18 @@ +# Metadata & Protocol + +Functions describe themselves through metadata — stability, examples, parameter info, ordering and +null semantics — which DuckDB reads for introspection and the query optimizer. The protocol and +invocation types model the request/response lifecycle on the wire. See the +[Metadata](../metadata.md) guide for authoring metadata via nested `Meta` classes. + +## Metadata + +::: vgi.metadata + +## Invocation lifecycle + +::: vgi.invocation + +## Protocol + +::: vgi.protocol diff --git a/docs/api/observability.md b/docs/api/observability.md new file mode 100644 index 0000000..bda42e5 --- /dev/null +++ b/docs/api/observability.md @@ -0,0 +1,19 @@ +# Observability + +!!! note "Advanced — reference only" + OpenTelemetry tracing and Sentry reporting are advanced, opt-in features without a dedicated + how-to guide yet. This page is the API reference. + +Workers can emit OpenTelemetry traces and structured logs. Tracing is a no-op unless the `[otel]` +extra is installed and a tracer provider is configured; logging configuration helpers shape worker +log output for the CLIs. + +## OpenTelemetry + +Requires `pip install vgi-python[otel]` for live tracing; otherwise a no-op tracer is used. + +::: vgi.otel + +## Logging configuration + +::: vgi.logging_config diff --git a/docs/api/storage.md b/docs/api/storage.md new file mode 100644 index 0000000..d7e08ce --- /dev/null +++ b/docs/api/storage.md @@ -0,0 +1,20 @@ +# State Storage + +Stateful functions (notably distributed aggregates) persist per-group state through a +`FunctionStorage` backend so it survives across worker invocations and processes. The default is +SQLite; Azure SQL and Cloudflare Durable Object backends are available for multi-worker +deployments. + +## Function storage + +::: vgi.function_storage + +## Azure SQL backend + +Requires `pip install vgi-python[azure]`. + +::: vgi.function_storage_azure_sql + +## Cloudflare Durable Object backend + +::: vgi.function_storage_cf_do diff --git a/docs/api/transactor.md b/docs/api/transactor.md new file mode 100644 index 0000000..e9711c8 --- /dev/null +++ b/docs/api/transactor.md @@ -0,0 +1,12 @@ +# Transactor + +!!! note "Advanced — reference only" + The transactor is an advanced feature without a dedicated how-to guide yet. This page is the + API reference; start from the [tutorial](../tutorial/index.md) and + [function patterns](../how-to/function-patterns.md) if you're new to VGI. + +The transactor is a long-lived subprocess that gives worker functions transactional access to a +database, mediated by `TransactorClient` over the `TransactorProtocol`. Requires +`pip install vgi-python[transactor]`. + +::: vgi.transactor diff --git a/docs/api/worker.md b/docs/api/worker.md new file mode 100644 index 0000000..bef01d8 --- /dev/null +++ b/docs/api/worker.md @@ -0,0 +1,23 @@ +# Worker & Serving + +A `Worker` hosts your functions (and optional catalog) in a separate process. It speaks the VGI +protocol over Arrow IPC — either stdin/stdout (subprocess transport) or HTTP. The `vgi-serve` +CLI (`vgi.serve`) is the zero-boilerplate entry point for running one. + +```python +from vgi import Worker, ScalarFunction + +class MyWorker(Worker): + functions = [MyScalarFunction()] + +# vgi-serve my_module:MyWorker # stdio +# vgi-serve my_module:MyWorker --http # HTTP +``` + +## Worker + +::: vgi.worker + +## Serving + +::: vgi.serve diff --git a/docs/assets/apple-touch-icon.png b/docs/assets/apple-touch-icon.png new file mode 100644 index 0000000..f17e82c Binary files /dev/null and b/docs/assets/apple-touch-icon.png differ diff --git a/docs/assets/favicon-16x16.png b/docs/assets/favicon-16x16.png new file mode 100644 index 0000000..e3c805d Binary files /dev/null and b/docs/assets/favicon-16x16.png differ diff --git a/docs/assets/favicon-32x32.png b/docs/assets/favicon-32x32.png new file mode 100644 index 0000000..dd46943 Binary files /dev/null and b/docs/assets/favicon-32x32.png differ diff --git a/docs/assets/favicon.ico b/docs/assets/favicon.ico new file mode 100644 index 0000000..8677db0 Binary files /dev/null and b/docs/assets/favicon.ico differ diff --git a/docs/assets/kinds/aggregate.svg b/docs/assets/kinds/aggregate.svg new file mode 100644 index 0000000..8f6f9f5 --- /dev/null +++ b/docs/assets/kinds/aggregate.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/docs/assets/kinds/buffering.svg b/docs/assets/kinds/buffering.svg new file mode 100644 index 0000000..10a3cf6 --- /dev/null +++ b/docs/assets/kinds/buffering.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/docs/assets/kinds/scalar.svg b/docs/assets/kinds/scalar.svg new file mode 100644 index 0000000..8e946f4 --- /dev/null +++ b/docs/assets/kinds/scalar.svg @@ -0,0 +1,7 @@ + + + + + ƒ + + diff --git a/docs/assets/kinds/table-in-out.svg b/docs/assets/kinds/table-in-out.svg new file mode 100644 index 0000000..69cdd25 --- /dev/null +++ b/docs/assets/kinds/table-in-out.svg @@ -0,0 +1,11 @@ + + + + + + + ƒ + + + + diff --git a/docs/assets/kinds/table.svg b/docs/assets/kinds/table.svg new file mode 100644 index 0000000..384082b --- /dev/null +++ b/docs/assets/kinds/table.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/docs/assets/logo.png b/docs/assets/logo.png new file mode 100644 index 0000000..f6cf175 Binary files /dev/null and b/docs/assets/logo.png differ diff --git a/docs/assets/social-card.png b/docs/assets/social-card.png new file mode 100644 index 0000000..866be10 Binary files /dev/null and b/docs/assets/social-card.png differ diff --git a/docs/concepts/index.md b/docs/concepts/index.md new file mode 100644 index 0000000..ebc2df6 --- /dev/null +++ b/docs/concepts/index.md @@ -0,0 +1,66 @@ +--- +description: "How VGI works: the worker process model, transports, the Apache Arrow data model, the call lifecycle, and parallel workers." +--- + +# Concepts + +**What this is:** the mental model behind VGI — enough to design workers correctly and debug them +confidently.
+**Who it's for:** developers who want the "why," not just the "how." None of this is +required to ship your first worker; do the [tutorial](../tutorial/index.md) for that. + +## The worker process model + +A VGI worker is an ordinary process — not code loaded into DuckDB. DuckDB launches your worker +(the `LOCATION` in `ATTACH`) and exchanges **Apache Arrow** record batches with it over a +transport. Because your code runs in its own process, it can use any Python library, can't crash +the database, and isn't tied to DuckDB's ABI or release cycle. + +## Transports + +The same worker runs over two transports without code changes: + +- **Subprocess** (default) — DuckDB (or the Python `Client`) spawns the worker and talks to it over + stdin/stdout. Best for local/co-located use; the simplest path. +- **HTTP** — the worker runs as a network service (`vgi-serve … --http`). Adds authentication, + externalized payloads, and stateless stream resume. See + [Serve over HTTP with auth](../how-to/http-auth.md). + +Per-call code can branch on the transport, but most workers never need to. + +## The Arrow data model + +Functions receive and return **columns**, not rows. A scalar function's `compute` gets a +`pa.StringArray`/`pa.Int64Array` (a whole column) and returns one the same length; table functions +emit `pa.RecordBatch`es. Operating on columns with `pyarrow.compute` is what keeps data transfer +and processing fast — there's no per-row Python call overhead, and the columnar bytes move without +re-serialization. Argument and result types are declared with `Annotated[...]`, and VGI derives the +Arrow schema (and thus the SQL signature) from them — see +[Argument Serialization](../argument-serialization.md). + +## The call lifecycle + +Every call flows through a small set of phases: + +- **bind** — declare the output schema from the arguments (and, for table-in-out, the input schema). +- **init** — set up per-call state (`initial_state`). +- **process** — called one or more times to produce output; a generator emits batches until + `out.finish()`. +- **finalize** — (aggregates / table-in-out) emit final results after all input is seen. + +Scalar functions use a simplified version (no `finalize`): each input batch maps to one output +batch until the input ends. The exact phase ordering, including the distributed/multi-worker case, +is diagrammed in the [Function Lifecycle reference](../lifecycle.md). + +## Parallel workers + +For throughput, the client can run **several worker processes** and distribute input batches across +them round-robin, collecting results. Aggregates merge partial state across workers via their +`combine` phase, which is why generator and aggregate state must be serializable (see +[Persist state across workers](../how-to/state-storage.md)). + +## Next steps + +- **Phase-by-phase detail** → [Function Lifecycle](../lifecycle.md). +- **How types cross the wire** → [Argument Serialization](../argument-serialization.md). +- **Apply it** → [How-to guides](../how-to/index.md). diff --git a/docs/contributing-docs.md b/docs/contributing-docs.md new file mode 100644 index 0000000..de9dd9e --- /dev/null +++ b/docs/contributing-docs.md @@ -0,0 +1,89 @@ +--- +description: "Conventions for writing vgi-python documentation: the per-page orientation standard, example rules, and a page template." +--- + +# Writing docs + +**What this is:** the conventions every vgi-python documentation page follows.
+**Who it's for:** anyone adding or editing docs. Following these keeps the site easy to orient in +and keeps examples from rotting. + +## Diátaxis: pick the right mode + +Every page belongs to exactly one of four modes. If a page is doing two jobs, split it. + +| Mode | Answers | Lives under | +|---|---|---| +| **Tutorial** | "Teach me, step by step, by doing." | `docs/tutorial/` | +| **How-to** | "How do I accomplish task X?" | `docs/how-to/` | +| **Concept** | "Why does it work this way?" | `docs/concepts/` | +| **Reference** | "What exactly is the signature/contract?" | `docs/api/` (auto-generated) | + +## The per-page orientation standard + +Every tutorial, how-to, and concept page **must** contain, in order: + +1. **A lead "what + who" block** — one sentence on what the page is, then **on its own line** + (use a trailing `
`) one phrase on who it's for, so a reader self-orients in under 10 seconds. + (See the top of this page.) +2. **Prerequisites** — assumed knowledge, prior steps, and required extras (e.g. + `pip install vgi-python[http]`), with links. Use a list or an admonition. +3. **At least one complete, runnable example** — no `...` elisions in the primary example. It must + pass the documentation-example tests (see below). *Exception:* advanced pages whose feature + isn't exercisable from a self-contained snippet (HTTP serving, auth, optimizer pushdown) may + lead with an illustrative `test="skip"` sketch — but label it illustrative and point to a + runnable worker or the reference for the real thing. +4. **A "Next steps" section** that advances the reader along the funnel: prefer a sibling **how-to** + or a **concept** page, then the **reference** for the full contract. Don't jump straight from a + how-to into auto-generated reference. No dead ends. + +## Example rules + +- **Prefer one source of truth.** Worker code lives in `examples/*.py` and is embedded with a + snippet so docs and tests share one file: + + ```text + ```python + --8<-- "examples/calc_worker.py" + ``` + ``` + +- **Examples must run in CI.** `examples/*.py` are imported and exercised by the test suite; + inline (non-snippet) Python blocks are executed by `tests/test_documentation_examples.py`. A + broken example fails the build. +- **Mark illustrative-only blocks.** If a block genuinely can't run standalone (SQL, shell, a + partial snippet), use a non-`python` fence or the ` ```python test="lint" ` / ` test="skip" ` + setting so the harness lints but doesn't execute it. **Do not put blank lines inside a + `test="skip"` block** — the renderer doesn't own that info string, so a blank line splits the + fence and leaks the delimiters into the page. Keep skip snippets blank-line-free. +- **Progressive disclosure for newcomers.** Put background that experts can skip inside a + collapsible admonition (`??? info "New to X?"`). + +## Page template + +Copy this skeleton when starting a new how-to or concept page: + +```markdown +--- +description: "One-line summary used for SEO and search." +--- + +# Page title + +**What this is:** one sentence.
+**Who it's for:** one phrase. + +## Prerequisites + +- ... + +## + +```python +--8<-- "examples/your_worker.py" +``` + +## Next steps + +- [Related page](...) +``` diff --git a/docs/how-to/catalogs.md b/docs/how-to/catalogs.md new file mode 100644 index 0000000..3495cee --- /dev/null +++ b/docs/how-to/catalogs.md @@ -0,0 +1,65 @@ +--- +description: "How to expose a VGI worker as a DuckDB catalog: schemas, function qualification, and tables/views via ATTACH." +--- + +# Expose a catalog + +**What this is:** how a worker presents itself to DuckDB as a catalog — a named namespace of +schemas, functions, tables, and views you reach with `ATTACH`.
+**Who it's for:** developers who've finished the [tutorial](../tutorial/index.md) and want to +understand how their functions get qualified names, or who want to expose data (not just functions). + +## Prerequisites + +- You can build and run a worker (see the [tutorial](../tutorial/index.md)). +- Familiarity with the function patterns is helpful: [Function patterns](function-patterns.md). + +## The model + +Every worker exposes one **`Catalog`** with a name. Inside it are one or more **`Schema`** +namespaces (DuckDB's default is `main`), each holding functions — and optionally tables and views. +You attach the catalog and address its contents by name: + +```sql +ATTACH 'calc' (TYPE vgi, LOCATION 'uv run calc_worker.py'); + +-- catalog.function (functions in `main` are reachable as catalog.name) +SELECT calc.double(21); + +-- catalog.schema.object (fully qualified) +SELECT * FROM calc.main.series(3); +``` + +The worker from the tutorial is exactly this — a catalog named `calc` with a `main` schema holding +the two functions: + +```python +--8<-- "examples/calc_worker.py" +``` + +The SQL name of a function is the snake_case of its class name (`Double` → `double`), unless you +override it with a `Meta.name` (as `sum_worker.py` does for `vgi_sum`). + +## Exposing data: tables and views + +A catalog can expose more than functions: + +- **`View`** — a named SQL query DuckDB evaluates. Pure SQL; no data provider needed: + + ```python + from vgi.catalog import View + View(name="recent", definition="SELECT * FROM calc.series(5)") + ``` + +- **`Table`** — a queryable table. Define it with an explicit `columns` schema (you supply the + scan) or back it with a `TableFunctionGenerator` so the schema is derived from the function. + +Both are passed to a `Schema(..., tables=[...], views=[...])`. The full set of options — +constraints, generated columns, column comments, filter requirements — is covered in the +[Catalog Interface reference](../catalog-interface.md). + +## Next steps + +- **Persist per-group state** → [State storage](state-storage.md). +- **Full catalog options** (tables, views, constraints) → [Catalog Interface reference](../catalog-interface.md). +- **Exact API** → [API Reference: Catalogs](../api/catalogs.md). diff --git a/docs/how-to/function-patterns.md b/docs/how-to/function-patterns.md new file mode 100644 index 0000000..26b4197 --- /dev/null +++ b/docs/how-to/function-patterns.md @@ -0,0 +1,227 @@ +--- +description: "How to write each of the four VGI function patterns: scalar, table, table-in-out, and aggregate — with a complete runnable worker for each." +--- + +# Function patterns + +**What this is:** a recipe showing each of the five VGI function patterns, with a complete, +runnable worker for each.
+**Who it's for:** developers who've finished the +[tutorial](../tutorial/index.md) and want to know which pattern fits their problem. + +## Prerequisites + +- You can build and run a worker (see the [tutorial](../tutorial/index.md)). +- Python 3.13+, `uv`, and a DuckDB-compatible engine (Haybarn or stock DuckDB). + +## Which pattern do I need? + + + +| Pattern | Shape | Use it when… | SQL | +|---|---|---|---| +| **Scalar** | 1 row → 1 row | you transform each row independently | `SELECT f(col) FROM t` | +| **Table** | args → N rows | you generate rows from arguments | `SELECT * FROM f(args)` | +| **Table-in-out** | N rows → M rows | you reshape/filter a streamed table | `SELECT * FROM f((SELECT …))` | +| **Aggregate** | grouped rows → 1 row/group | you accumulate per `GROUP BY` group | `SELECT f(col) FROM t GROUP BY k` | +| **Buffering** | stream → [state] → stream | you must see *every* row first (sort, top-k, full reduction) | `SELECT * FROM f((SELECT …))` | + +## Scalar + +
+![scalar shape](../assets/kinds/scalar.svg){ .kind-banner__glyph } +
+`1 row → 1 value`{ .kind-banner__formula } + +Runs on each row independently and returns a single value — a pure per-row transform. +{ .kind-banner__desc } +
+
+ +One row in, one row out. Operate on the whole column with `pyarrow.compute`; the `Annotated` +types are the schema. + +```python +--8<-- "examples/calc_scalar_worker.py" +``` + +```sql +ATTACH 'calc' (TYPE vgi, LOCATION 'uv run calc_scalar_worker.py'); +SELECT calc.double(n) FROM (VALUES (1), (2), (3)) AS t(n); +``` + +Scalars aren't just for numbers — any column type works. A string transform looks the same; here +`compute` joins `Hello, ` + name + `!` across the column: + +```python +--8<-- "examples/greeting_scalar_worker.py" +``` + +```sql +SELECT greetings.greeting(name) FROM (VALUES ('Alice'), ('Bob')) AS t(name); +``` + +## Table + +
+![table shape](../assets/kinds/table.svg){ .kind-banner__glyph } +
+`args → N rows`{ .kind-banner__formula } + +A table-valued source: scalar arguments in, a whole set of rows out. +{ .kind-banner__desc } +
+
+ +Generate rows from arguments, no input table. Declare a typed args dataclass and a `FIXED_SCHEMA`; +`process` emits batches until `out.finish()`. The `@bind_fixed_schema` / `@init_single_worker` +decorators wire up the common single-worker lifecycle. (This is the full tutorial worker — the +scalar `double` plus the `series` generator.) + +```python +--8<-- "examples/calc_worker.py" +``` + +```sql +ATTACH 'calc' (TYPE vgi, LOCATION 'uv run calc_worker.py'); +SELECT * FROM calc.series(3); +``` + +### Streaming with state + +The tutorial's `series` emits every row in a single `process` call. That's fine for small results, +but `process` is actually called *repeatedly* until you call `out.finish()` — so for large output +you emit a bounded chunk per call and remember your place in a small **state** object. The state +extends `ArrowSerializableDataclass` so it survives HTTP state round-trips: + +```python +--8<-- "examples/series_streaming_worker.py" +``` + +```sql +SELECT * FROM calc.series(1000000); -- streamed CHUNK rows per process() call +``` + +## Table-in-out + +
+![table-in-out shape](../assets/kinds/table-in-out.svg){ .kind-banner__glyph } +
+`N rows → M rows`{ .kind-banner__formula } + +Consumes a relation and streams a transformed relation back, batch by batch. +{ .kind-banner__desc } +
+
+ +Stream an input table through, batch by batch, emitting transformed output. `on_bind` declares the +output schema; `process` receives each input `batch` and emits results. Here we keep only rows +whose `value` column is positive. + +```python +--8<-- "examples/filter_worker.py" +``` + +```sql +ATTACH 'filters' (TYPE vgi, LOCATION 'uv run filter_worker.py'); +SELECT * FROM filters.filter_positive((SELECT * FROM my_table)); +``` + +## Aggregate + +
+![aggregate shape](../assets/kinds/aggregate.svg){ .kind-banner__glyph } +
+`N rows → 1 value`{ .kind-banner__formula } + +Folds many rows down into a single value per group. +{ .kind-banner__desc } +
+
+ +Accumulate input rows into per-group state, then emit one row per group. Aggregates are driven by +DuckDB's `GROUP BY` and run in three phases — `update` (fold a batch into per-group state), +`combine` (merge partial states across parallel workers), and `finalize` (state → output row). + +```python +--8<-- "examples/sum_worker.py" +``` + +```sql +ATTACH 'aggregates' (TYPE vgi, LOCATION 'uv run sum_worker.py'); +SELECT category, aggregates.vgi_sum(value) FROM t GROUP BY category; +``` + +??? info "State must be an `ArrowSerializableDataclass`" + Table generators and aggregates keep state between calls. Because that state is serialized + across parallel workers (aggregates) and HTTP round-trips, the framework requires it to extend + `ArrowSerializableDataclass`. Annotate fields with `ArrowType(...)` so the wire type is + explicit. + +## Buffering + +
+![buffering shape](../assets/kinds/buffering.svg){ .kind-banner__glyph } +
+`stream → [state] → stream`{ .kind-banner__formula } + +Holds every input row in state before emitting — the basis for sorts, top-k, and full-stream reductions. +{ .kind-banner__desc } +
+
+ +When a function must see the **whole** input before it can produce *any* output — a global sort, +top-k, or a full reduction — use a buffering function. Unlike table-in-out (which emits per input +batch), it runs in three phases: `process` (the **sink** — stash each batch, return a `state_id`), +`combine` (reduce all the partials once), and `finalize` (the **source** — stream the result out). +Because the phases can run in different worker processes, state can't live in memory — it goes in +`params.storage`, a shared store keyed to this call (its `execution_id`). See +[Persist state across workers](state-storage.md) for the backends behind it. + +```python +--8<-- "examples/row_count_worker.py" +``` + +```sql +ATTACH 'buffers' (TYPE vgi, LOCATION 'uv run row_count_worker.py'); +SELECT * FROM buffers.row_count((SELECT * FROM big_table)); +``` + +??? info "Buffering vs. table-in-out" + Both consume a relation, but a **table-in-out** function emits *per input batch* and never + holds the whole input — use it for streaming transforms (filter, enrich, reshape). Reach for + **buffering** only when output genuinely depends on every row. See + [Persist state across workers](state-storage.md) for the storage backends `params.storage` uses. + +## Next steps + +- **Persist state across workers** → [State storage](state-storage.md). +- **Let the optimizer prune work** → [Integrate with the optimizer](pushdown-and-statistics.md). +- **Understand the call lifecycle** → [Concepts](../concepts/index.md). +- **Exact signatures** → [API Reference: Functions](../api/functions.md). diff --git a/docs/how-to/http-auth.md b/docs/how-to/http-auth.md new file mode 100644 index 0000000..b4cf460 --- /dev/null +++ b/docs/how-to/http-auth.md @@ -0,0 +1,70 @@ +--- +description: "How to serve a VGI worker over HTTP and authenticate callers with bearer tokens or JWT/OAuth." +--- + +# Serve over HTTP with authentication + +**What this is:** how to run a worker over **HTTP** instead of a subprocess, and gate it with +authentication.
+**Who it's for:** developers deploying a worker as a network service.
+**Requires:** `pip install vgi-python[http]` (and `[oauth]` for JWT). + +## Prerequisites + +- A working worker (see the [tutorial](../tutorial/index.md)). +- `pip install vgi-python[http]`; for JWT/OAuth also `vgi-python[oauth]`. + +## Serve over HTTP + +The same worker that runs over a subprocess also runs over HTTP — add `--http`: + +```bash +vgi-serve my_worker.py --http +``` + +DuckDB still attaches the worker over HTTP the usual way (`ATTACH ... (TYPE vgi, LOCATION +'http://...')`). You only need the **Python `Client`** when you're calling the worker *from Python* +rather than from SQL — for tests, scripts, or another service. It connects with `transport="http"` +instead of spawning a subprocess, and exposes the same call methods: + +```python +# illustrative — calling the worker from Python over HTTP +from vgi.client import Client + +with Client(transport="http", base_url="http://localhost:8080", bearer_token="token1") as client: + ... # same .scalar_function() / .table_function() calls as the subprocess transport +``` + +## Add authentication + +The quickest setup is static bearer tokens via an environment variable — comma-separated +`token=principal` pairs: + +```bash +VGI_BEARER_TOKENS="token1=alice,token2=bob" vgi-serve my_worker.py --http +``` + +Unauthenticated requests get HTTP 401. Authenticated requests carry the principal in an +`AuthContext`. Your function reads it through an optional `ctx` parameter: declare `ctx` in the +signature and the framework injects a per-call `CallContext` (auth, logging, transport info) — you +don't pass it from SQL. + +```python +# illustrative — reading the injected ctx in a function +class Secret(ScalarFunction): + @classmethod + def compute(cls, value, *, ctx) -> ...: + ctx.auth.require_authenticated() # raises if anonymous + principal = ctx.auth.principal # "alice" + ... +``` + +For JWT/JWKS and RFC 9728 OAuth resource metadata, set `VGI_JWT_ISSUER` / `VGI_JWT_AUDIENCE` +(requires `vgi-python[oauth]`) — see the [Authentication reference](../authentication.md) for the +full variable list and programmatic `make_wsgi_app(authenticate=...)` API. + +## Next steps + +- **Full auth options** (JWT, OAuth metadata, custom callbacks) → + [Authentication reference](../authentication.md). +- **Request context** (`ctx.auth`, logging) → [API Reference: Auth & Secrets](../api/auth.md). diff --git a/docs/how-to/index.md b/docs/how-to/index.md new file mode 100644 index 0000000..70281c3 --- /dev/null +++ b/docs/how-to/index.md @@ -0,0 +1,35 @@ +--- +description: "Task-oriented recipes for building VGI workers: function patterns, catalogs, state, auth, and optimizer integration." +--- + +# How-to guides + +**What this is:** focused, task-oriented recipes for getting a specific thing done.
+**Who it's for:** developers who've finished the [tutorial](../tutorial/index.md) and want to build +something real. Each guide assumes you can already write and run a basic worker. + +## Recipes + +- **[Function patterns](function-patterns.md)** — scalar, table, table-in-out, and aggregate + functions, with a runnable worker for each. *(Start here.)* +- **[Expose a catalog](catalogs.md)** — surface schemas, functions, tables, and views to DuckDB + via `ATTACH`. +- **[Persist state across workers](state-storage.md)** — shared, durable state for distributed + aggregates. +- **[Serve over HTTP with auth](http-auth.md)** — run a worker as a network service and gate it + with bearer/JWT auth. +- **[Integrate with the optimizer](pushdown-and-statistics.md)** — accept pushed-down filters and + report column statistics. +Each recipe ends with a **Next steps** section that links onward to a concept page and the full +reference for that topic. + +## Reference & tooling + +- **[Function Metadata](../metadata.md)** — describe your functions for introspection. +- **[CLI](../cli.md)** — invoke functions and inspect workers from the shell. + +## Next steps + +- New here? Start with the [tutorial](../tutorial/index.md). +- Want the "why" behind the API? See [Concepts](../concepts/index.md). +- Need exact signatures? See the [API Reference](../api/index.md). diff --git a/docs/how-to/pushdown-and-statistics.md b/docs/how-to/pushdown-and-statistics.md new file mode 100644 index 0000000..8bb6fd1 --- /dev/null +++ b/docs/how-to/pushdown-and-statistics.md @@ -0,0 +1,72 @@ +--- +description: "How to integrate VGI table functions with DuckDB's optimizer: accept pushed-down filters and report column statistics." +--- + +# Integrate with the optimizer + +**What this is:** how to make table functions cooperate with DuckDB's query optimizer — receiving +pushed-down `WHERE` predicates and reporting column statistics so the planner can skip work.
+**Who it's for:** developers whose table functions back real data sources and want less data moved. + +## Prerequisites + +- A table or table-in-out function (see [Function patterns](function-patterns.md)). +- Familiarity with your data's shape (which columns are filterable, their ranges). + +## Filter pushdown + +A table function can receive the `WHERE` predicates DuckDB would otherwise apply *after* the scan, +and apply them at the source. Opt in with `filter_pushdown = True` in the function's `Meta`. The +framework deserializes the predicates for you and exposes them on `params.current_pushdown_filters` +as a `PushdownFilters` tree (or `None` when no filter applies), refreshed before each `process` +call: + +```python +# illustrative — sketch using your own types +class Events(TableFunctionGenerator[EventsArgs]): + class Meta: + filter_pushdown = True # opt in to receiving WHERE predicates + + @classmethod + def process(cls, params, state, out): + filters = params.current_pushdown_filters # PushdownFilters tree, or None + # apply the filters while generating rows, then out.emit(...) / out.finish() +``` + +`PushdownFilters` is already decoded — you don't call `deserialize_filters` yourself (that helper is +for the raw wire bytes). To have the framework apply the filters to your output automatically, +set `auto_apply_filters = True` in `Meta`. The node types and a worked example are in the +[Filter Pushdown reference](../filter-pushdown.md) and [API: Filter Pushdown](../api/filters.md). + +## Column statistics + +When a table reports per-column min/max, null, and distinct-count statistics, DuckDB's optimizer +can eliminate scans and order joins better. The declarative path is a `statistics` entry on the +`Table` descriptor: + +```python +# illustrative — `schema` and the table stand in for your own catalog +from vgi.catalog import Table +from vgi.catalog.descriptors import ColumnStatisticsInput + +Table( + name="departments", + columns=schema, + statistics={"id": ColumnStatisticsInput(min=1, max=10, has_null=False)}, +) +``` + +```sql +-- With statistics the optimizer can prune an impossible predicate entirely: +EXPLAIN SELECT * FROM mydb.data.departments WHERE id > 100; -- Physical Plan: EMPTY_RESULT +``` + +(The snippets above are illustrative — `schema` and the `departments` table stand in for your own +catalog.) Full details — RPC-based dynamic statistics, TTLs, spatial bounds — are in the +[Column Statistics reference](../column-statistics.md). + +## Next steps + +- **Filter format & evaluation** → [Filter Pushdown reference](../filter-pushdown.md) · + [API: Filter Pushdown](../api/filters.md). +- **Statistics options** → [Column Statistics reference](../column-statistics.md). diff --git a/docs/how-to/state-storage.md b/docs/how-to/state-storage.md new file mode 100644 index 0000000..03381c0 --- /dev/null +++ b/docs/how-to/state-storage.md @@ -0,0 +1,69 @@ +--- +description: "How to give VGI functions shared, persistent state across worker processes — the default SQLite backend and cloud alternatives." +--- + +# Persist state across workers + +**What this is:** how functions that span **multiple worker processes** (notably distributed +aggregates) share and persist state.
+**Who it's for:** developers building aggregates or any +function that coordinates partial results across workers. + +## Prerequisites + +- You've built an aggregate or multi-worker function (see + [Function patterns → Aggregate](function-patterns.md#aggregate)). +- For the Azure backend: `pip install vgi-python[azure]`. SQLite and Cloudflare DO need no extra. + +!!! note "`vgi-serve`" + The commands below use `vgi-serve`, the CLI installed with `vgi-python` that runs a worker + module as a long-lived process (the production counterpart to the tutorial's `uv run`). The + `--http` flag serves it over HTTP instead of stdin/stdout. + +## Two kinds of "state" — don't confuse them + +- **Generator cursor state** — the small `ArrowSerializableDataclass` a table generator keeps + *within one scan* (see [streaming with state](function-patterns.md#streaming-with-state)). It + lives in the worker for the duration of the call. +- **Shared storage** (this page) — state that must outlive a single call or be shared across + **separate worker processes**, e.g. combining partial aggregate results. This is backed by a + pluggable store. + +## The default: SQLite (zero config) + +Under the subprocess transport, shared storage "just works" — all workers share a local SQLite +database (WAL mode) at the platform state directory. Nothing to configure: + +```bash +vgi-serve my_worker.py +``` + +## Choosing a backend + +Select a backend with the `VGI_WORKER_SHARED_STORAGE` environment variable: + +```bash +# Local / subprocess (default) +VGI_WORKER_SHARED_STORAGE=sqlite vgi-serve my_worker.py + +# Azure cloud (requires vgi-python[azure]) +VGI_WORKER_SHARED_STORAGE=azure-sql vgi-serve my_worker.py --http + +# Edge / multi-cloud +VGI_WORKER_SHARED_STORAGE=cloudflare-do vgi-serve my_worker.py --http +``` + +| Backend | Value | Use case | Dependencies | +|---|---|---|---| +| SQLite | `sqlite` (default) | local / subprocess | none (stdlib) | +| Azure SQL | `azure-sql` | Azure deployments | `vgi-python[azure]` | +| Cloudflare DO | `cloudflare-do` | edge / multi-cloud | none extra — uses `httpx`, which ships with `vgi-python`; needs a Worker endpoint + token | + +The per-backend setup (connection strings, credentials, table provisioning) is documented in the +[Shared Storage reference](../shared-storage.md). + +## Next steps + +- **Aggregates** that use this → [Function patterns → Aggregate](function-patterns.md#aggregate). +- **Full backend setup** → [Shared Storage reference](../shared-storage.md). +- **Exact API** → [API Reference: State storage](../api/storage.md). diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..0391b60 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,114 @@ +--- +description: "vgi-python: add scalar, table, and aggregate functions to DuckDB in pure Python over Apache Arrow — no C++ to compile, no extension to version, no build step." +--- + +# vgi-python + +**Extend DuckDB in pure Python.** Add scalar, table, and aggregate functions that run in your own +process and stream data to DuckDB over Apache Arrow — no C++ to compile, no extension to version, +no build step. + +Write a function, `uv run` the script, query it from SQL. Other languages work too. + +

+ VGI logo +

+ +

+Built by [🚜 Query.Farm](https://query.farm). +

+ +## See it in action + +A **scalar** function — one row in, one row out: + +```python +--8<-- "examples/calc_worker.py:scalar" +``` + +A **table** function — generate rows from an argument: + +```python +--8<-- "examples/calc_worker.py:table" +``` + +Drop both into a `Worker`, add an [inline-script-metadata](https://packaging.python.org/en/latest/specifications/inline-script-metadata/) +header, and `uv run` it — no virtualenv, nothing to `pip install`. (The +[tutorial](tutorial/index.md) builds the complete file step by step.) + +```sql +INSTALL vgi FROM community; +LOAD vgi; +-- LOCATION is the command that launches the worker. +ATTACH 'calc' (TYPE vgi, LOCATION 'uv run calc_worker.py'); + +SELECT calc.double(21); -- 42 +SELECT * FROM calc.series(3); -- 0, 1, 2 +``` + +That's it. No compilation, no extension versioning, no build process. + +[Build this worker step by step in the tutorial →](tutorial/index.md){ .md-button } + +## Installation + +The package is published on PyPI as `vgi-python` (the `vgi` name was taken), but you `import vgi` +in code: + +```bash +pip install vgi-python # or: uv add vgi-python +``` + +You also need a DuckDB-compatible engine. [Haybarn](https://github.com/Query-farm-haybarn/haybarn), +Query.Farm's DuckDB distribution, ships the `vgi` extension and runs with no install: + +```bash +uvx haybarn-cli # interactive SQL session +``` + +Stock `duckdb` works too — `INSTALL vgi FROM community; LOAD vgi;`. + +## Why VGI? + +| Traditional extensions | VGI workers | +|---|---| +| C/C++ compilation required | Any language with an Apache Arrow library | +| Tied to a DuckDB version | Version independent | +| Complex build/release cycle | Ship a script or executable | +| Runs in DuckDB's process | Isolated worker process — a crash or a heavy dependency can't take DuckDB down | +| Native code in DuckDB's threads | Your code, optionally fanned out across worker processes | + +**The tradeoff:** data crosses a process boundary as Apache Arrow IPC. That's fast and columnar, +but not free — co-locate workers (subprocess transport) for latency-sensitive paths, and reach for +VGI when the productivity and isolation win outweighs the hop. + +**Use cases:** call REST APIs from SQL, run ML inference, process data with pandas/numpy, build +custom ETL transforms, expose external data sources as queryable tables and views. + +## Function patterns + +| Shape | Type | Base class | Use case | +|---|---|---|---| +| ![scalar shape](assets/kinds/scalar.svg){ height="32" } | **Scalar** | `ScalarFunction` | Per-row transforms (1:1) | +| ![table shape](assets/kinds/table.svg){ height="32" } | **Table** | `TableFunctionGenerator` | Generate data | +| ![table-in-out shape](assets/kinds/table-in-out.svg){ height="32" } | **Table-in-out** | `TableInOutFunction` | Streaming transforms, filtering | +| ![aggregate shape](assets/kinds/aggregate.svg){ height="32" } | **Aggregate** | `AggregateFunction` | Grouped accumulation | +| ![buffering shape](assets/kinds/buffering.svg){ height="32" } | **Buffering** | `TableBufferingFunction` | Sees every row first (sort, top-k) | + +See the [API Reference](api/index.md) for the full surface, or jump into the guides below. + +## Documentation + +- **[Tutorial](tutorial/index.md)** — build your first worker (scalar + table function callable + from DuckDB) in about 20 minutes. **Start here.** +- **[How-to guides](how-to/index.md)** — task-oriented recipes: function patterns, catalogs, + state, auth/HTTP, and optimizer integration. +- **[Concepts](concepts/index.md)** — how it works: the worker lifecycle, transports, and the + Arrow data model. +- **[API Reference](api/index.md)** — auto-generated from the source, organized by module. + +## Project links + +- Source: [github.com/Query-farm/vgi-python](https://github.com/Query-farm/vgi-python) +- PyPI: [vgi-python](https://pypi.org/project/vgi-python/) +- Built on [vgi-rpc](https://vgi-rpc-python.query.farm/) — the transport-agnostic RPC layer. diff --git a/docs/overrides/main.html b/docs/overrides/main.html new file mode 100644 index 0000000..2f51abb --- /dev/null +++ b/docs/overrides/main.html @@ -0,0 +1,45 @@ +{% extends "base.html" %} + +{% block extrahead %} + {% if page and page.meta and page.meta.description %} + {% set page_desc = page.meta.description %} + {% else %} + {% set page_desc = config.site_description %} + {% endif %} + {% if page and page.is_homepage %} + {% set page_title = config.site_name ~ " — Connect DuckDB to external programs via Apache Arrow" %} + {% elif page and page.title %} + {% set page_title = page.title ~ " - " ~ config.site_name %} + {% else %} + {% set page_title = config.site_name %} + {% endif %} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +{% endblock %} diff --git a/docs/robots.txt b/docs/robots.txt new file mode 100644 index 0000000..d78bace --- /dev/null +++ b/docs/robots.txt @@ -0,0 +1,4 @@ +User-agent: * +Allow: / + +Sitemap: https://vgi-python.query.farm/sitemap.xml diff --git a/docs/stylesheets/extra.css b/docs/stylesheets/extra.css new file mode 100644 index 0000000..7ca338e --- /dev/null +++ b/docs/stylesheets/extra.css @@ -0,0 +1,389 @@ +/* Query.Farm palette — earthy greens, warm golds, and cream tones + Matched to https://vgi-rpc.query.farm website theme */ + +/* ── CSS custom properties (farm palette) ── */ +:root { + --farm-green-deep: #2d5016; + --farm-green-field: #4a7c23; + --farm-green-light: #6ba034; + --farm-gold: #c8a43a; + --farm-gold-light: #d4b64e; + --farm-brown: #6b4423; + --farm-cream: #faf8f0; + --farm-cream-dark: #f0ece0; + --farm-dark: #1a1a0e; + --farm-dark-card: #252518; + --farm-dark-border: #3a3a28; + --farm-text: #2c2c1e; + --farm-text-muted: #6b6b5a; + --farm-text-light: #f5f0e0; + --farm-text-light-muted: #b8b0a0; +} + +/* ── Light mode ── */ +[data-md-color-scheme="default"] { + --md-primary-fg-color: var(--farm-green-deep); + --md-primary-fg-color--light: var(--farm-green-field); + --md-primary-fg-color--dark: #1e3a0e; + --md-primary-bg-color: #ffffff; + --md-primary-bg-color--light: #ffffffb3; + --md-accent-fg-color: var(--farm-gold); + --md-accent-fg-color--transparent: #c8a43a33; + --md-accent-bg-color: #ffffff; + --md-accent-bg-color--light: #ffffffb3; + --md-default-bg-color: var(--farm-cream); + --md-default-bg-color--light: var(--farm-cream-dark); + --md-default-fg-color: var(--farm-text); + --md-default-fg-color--light: var(--farm-text-muted); + --md-typeset-color: var(--farm-text); + --md-code-bg-color: #f5f0e6; +} + +/* Dark mode */ +[data-md-color-scheme="slate"] { + --md-primary-fg-color: var(--farm-green-field); + --md-primary-fg-color--light: var(--farm-green-light); + --md-primary-fg-color--dark: var(--farm-green-deep); + --md-primary-bg-color: var(--farm-text-light); + --md-primary-bg-color--light: #f5f0e0b3; + --md-accent-fg-color: var(--farm-gold-light); + --md-accent-fg-color--transparent: #d4b64e33; + --md-accent-bg-color: #000000; + --md-accent-bg-color--light: #0000004d; + --md-default-bg-color: var(--farm-dark); + --md-default-bg-color--light: var(--farm-dark-card); + --md-default-bg-color--lighter: #25251833; + --md-default-bg-color--lightest: #2525180d; + --md-default-fg-color: var(--farm-text-light); + --md-default-fg-color--light: var(--farm-text-light-muted); + --md-typeset-color: var(--farm-text-light); + --md-code-bg-color: var(--farm-dark-card); +} + +/* ── Header / nav bar ── */ +[data-md-color-scheme="default"] .md-header { + background-color: var(--farm-green-deep); +} + +[data-md-color-scheme="slate"] .md-header { + background-color: var(--farm-dark); + border-bottom: 1px solid var(--farm-dark-border); +} + +[data-md-color-scheme="default"] .md-tabs { + background-color: var(--farm-green-deep); +} + +[data-md-color-scheme="slate"] .md-tabs { + background-color: var(--farm-dark); + border-bottom: 1px solid var(--farm-dark-border); +} + +/* Nav tabs — gold underline on active/hover */ +.md-tabs__link--active, +.md-tabs__link:hover { + border-bottom: 2px solid var(--farm-gold); +} + +/* ── Sidebar navigation ── */ +[data-md-color-scheme="default"] .md-sidebar { + border-right: 1px solid var(--farm-cream-dark); +} + +[data-md-color-scheme="slate"] .md-sidebar { + border-right: 1px solid var(--farm-dark-border); +} + +/* ── Admonition styling — farm tones ── */ +.md-typeset .admonition.farm, +.md-typeset details.farm { + border-color: var(--farm-green-field); +} +.md-typeset .farm > .admonition-title, +.md-typeset .farm > summary { + background-color: #4a7c231a; + border-color: var(--farm-green-field); +} + +/* ── Code block styling ── */ +[data-md-color-scheme="default"] .highlight code { + border-left: 3px solid var(--farm-green-field); +} + +/* Inline code */ +[data-md-color-scheme="default"] .md-typeset code { + background-color: var(--farm-cream-dark); + color: var(--farm-text); +} + +[data-md-color-scheme="slate"] .md-typeset code { + background-color: var(--farm-dark-card); + color: var(--farm-text-light); +} + +/* ── Links ── */ +[data-md-color-scheme="default"] .md-typeset a { + color: var(--farm-green-deep); +} + +[data-md-color-scheme="default"] .md-typeset a:hover { + color: var(--farm-green-field); +} + +[data-md-color-scheme="slate"] .md-typeset a { + color: var(--farm-green-light); +} + +[data-md-color-scheme="slate"] .md-typeset a:hover { + color: var(--farm-gold-light); +} + +/* ── Footer ── */ +.md-footer { + border-top: 1px solid var(--farm-cream-dark); +} + +[data-md-color-scheme="default"] .md-footer-meta { + background-color: var(--farm-green-deep); +} + +[data-md-color-scheme="slate"] .md-footer { + border-top: 1px solid var(--farm-dark-border); +} + +[data-md-color-scheme="slate"] .md-footer-meta { + background-color: var(--farm-dark-card); +} + +/* ── Tables — match website styling ── */ +[data-md-color-scheme="default"] .md-typeset table:not([class]) th { + background-color: var(--farm-cream-dark); + color: var(--farm-text); +} + +[data-md-color-scheme="slate"] .md-typeset table:not([class]) th { + background-color: var(--farm-dark-card); + color: var(--farm-text-light); +} + +[data-md-color-scheme="default"] .md-typeset table:not([class]) td { + border-color: var(--farm-cream-dark); +} + +[data-md-color-scheme="slate"] .md-typeset table:not([class]) td { + border-color: var(--farm-dark-border); +} + +/* ── Search ── */ +[data-md-color-scheme="default"] .md-search__input { + background-color: #ffffff; +} + +[data-md-color-scheme="slate"] .md-search__input { + background-color: var(--farm-dark-card); +} + +/* ── Hero section on landing page ── */ +.hero { + padding: 2rem 0; + margin-bottom: 2rem; + text-align: center; +} + +.hero h1 { + font-size: 2.5rem; + margin-bottom: 0.5rem; +} + +.hero .tagline { + font-size: 1.2rem; + opacity: 0.8; + margin-bottom: 1rem; +} + +.hero .built-by { + font-size: 1.15rem; + opacity: 0.85; +} + +.hero .built-by a { + color: var(--farm-green-deep); + font-weight: 700; +} + +[data-md-color-scheme="slate"] .hero .built-by a { + color: var(--farm-green-light); +} + +/* Hero logo */ +.hero-logo { + display: flex; + justify-content: center; + margin-bottom: 1rem; +} + +.hero-logo-img { + border-radius: 50%; + box-shadow: 0 4px 24px rgba(0, 0, 0, 0.18); + transition: transform 0.3s ease; + width: 200px; + height: 200px; +} + +.hero-logo-img:hover { + transform: scale(1.05); +} + +/* Header logo — slightly bigger than default, full logo at its natural aspect ratio */ +.md-header__button.md-logo img, +.md-nav__button.md-logo img { + height: 1.8rem; + width: auto; +} + +/* ── D2 diagrams — constrain size and center ── */ +.md-typeset .d2 { + max-width: 360px !important; + margin: 1rem auto !important; +} + +.md-typeset .d2 svg { + width: 100% !important; + height: auto !important; +} + +/* ── Content area cards/separators ── */ +[data-md-color-scheme="default"] .md-typeset hr { + border-color: var(--farm-cream-dark); +} + +[data-md-color-scheme="slate"] .md-typeset hr { + border-color: var(--farm-dark-border); +} + +/* ── Function-kind banner ── + A header card for each function shape: the cardinality glyph alongside its + formula and a one-line gloss, tinted in the kind's colour. Ported from the + VGI Java docs' KindBanner. Markdown usage (md_in_html + attr_list): + +
+ ![scalar shape](../assets/kinds/scalar.svg){ .kind-banner__glyph } +
+ `1 row → 1 value`{ .kind-banner__formula } + One-line description of the shape. + { .kind-banner__desc } +
+
+*/ +.md-typeset .kind-banner { + display: flex; + align-items: center; + gap: 20px; + margin: 1.25rem 0 2rem; + padding: 16px 20px; + border: 1px solid var(--md-default-fg-color--lightest); + border-left: 4px solid var(--kind); + border-radius: 12px; + background: color-mix(in srgb, var(--kind) 7%, var(--md-default-bg-color)); +} +.md-typeset .kind-banner > p { + margin: 0; + flex: none; +} +.md-typeset .kind-banner__glyph { + flex: none; + height: 48px; + width: auto; + margin: 0; + box-shadow: none; + background: none; +} +.md-typeset .kind-banner__text { + min-width: 0; +} +.md-typeset .kind-banner__formula { + display: inline-block; + font-weight: 700; + font-size: 0.78rem; + color: var(--kind); + background: color-mix(in srgb, var(--kind) 14%, transparent); + padding: 2px 9px; + border-radius: 6px; +} +.md-typeset .kind-banner__desc { + margin: 0.5rem 0 0; + font-size: 0.82rem; + line-height: 1.5; + color: var(--md-default-fg-color--light); +} + +.md-typeset .kind-banner--scalar { --kind: #2563eb; } +.md-typeset .kind-banner--table { --kind: #0d9488; } +.md-typeset .kind-banner--table-in-out { --kind: #7c3aed; } +.md-typeset .kind-banner--aggregate { --kind: #d97706; } +.md-typeset .kind-banner--buffering { --kind: #e11d48; } + +/* ── Function-shape gallery ── + A strip of all five kinds laid out side by side so the shared shape language + is visible at a glance. Ported from the VGI Java docs' KindGallery. Markdown + usage (md_in_html + attr_list): a .kind-gallery div of .kind-gallery__card + anchors, each carrying a kind modifier class. */ +.md-typeset .kind-gallery { + display: grid; + grid-template-columns: repeat(5, 1fr); + gap: 14px; + margin: 1.5rem 0 2rem; +} +.md-typeset .kind-gallery__card, +.md-typeset .kind-gallery__card:hover { + display: flex; + flex-direction: column; + align-items: center; + gap: 10px; + padding: 20px 14px 16px; + text-align: center; + text-decoration: none; + border: 1px solid var(--md-default-fg-color--lightest); + border-radius: 14px; + background: color-mix(in srgb, var(--kind) 6%, var(--md-default-bg-color)); + transition: border-color 0.2s, transform 0.2s, box-shadow 0.2s; +} +.md-typeset .kind-gallery__card:hover { + border-color: var(--kind); + transform: translateY(-3px); + box-shadow: 0 10px 28px -18px var(--kind); +} +.md-typeset .kind-gallery__card img { + height: 40px; + width: auto; + margin: 0; + box-shadow: none; + background: none; +} +.md-typeset .kind-gallery__name { + font-weight: 700; + font-size: 0.8rem; + color: var(--md-default-fg-color); +} +.md-typeset .kind-gallery__formula { + max-width: 100%; + font-size: 0.68rem; + font-weight: 600; + color: var(--kind); + background: color-mix(in srgb, var(--kind) 12%, transparent); + padding: 2px 8px; + border-radius: 6px; + white-space: normal; + overflow-wrap: break-word; +} +.md-typeset .kind-gallery--scalar { --kind: #2563eb; } +.md-typeset .kind-gallery--table { --kind: #0d9488; } +.md-typeset .kind-gallery--table-in-out { --kind: #7c3aed; } +.md-typeset .kind-gallery--aggregate { --kind: #d97706; } +.md-typeset .kind-gallery--buffering { --kind: #e11d48; } + +@media screen and (max-width: 60em) { + .md-typeset .kind-gallery { + grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); + } +} diff --git a/docs/tutorial/index.md b/docs/tutorial/index.md new file mode 100644 index 0000000..248ebf0 --- /dev/null +++ b/docs/tutorial/index.md @@ -0,0 +1,33 @@ +--- +description: "Build your first VGI worker — a scalar then a table function callable from DuckDB — in about 20 minutes." +--- + +# Tutorial: build your first worker + +**What this is:** a two-step path from an empty file to custom functions callable from SQL.
+**Who it's for:** anyone who wants to extend DuckDB with Python — no prior VGI knowledge assumed. +Budget about **20 minutes** total. + +## Prerequisites + +- **Python 3.13+** and **[uv](https://docs.astral.sh/uv/)** installed (`uv --version`). +- A terminal. You do **not** need to install vgi-python or DuckDB first, or create a virtualenv — + `uv run` handles dependencies from the script itself. + +??? info "New to DuckDB extensions or Apache Arrow?" + A **VGI worker** is just a Python process that DuckDB talks to over [Apache + Arrow](https://arrow.apache.org/) — a columnar in-memory format. Your functions receive and + return Arrow `RecordBatch`es (columns of data) rather than row-by-row values, which is what + makes the transfer fast. You don't need to know Arrow deeply to finish this tutorial; we point + out the few places it matters. + +## What you'll build + +A worker exposing a catalog named `calc` with two functions, one per step: + +1. **[Your first scalar function](scalar.md)** — `double(value)` maps one row to one row + (`21` → `42`). *(~10 minutes, gets you a working query.)* +2. **[Add a table function](table.md)** — `series(count)` generates `count` rows from an argument. + *(~10 minutes.)* + +Start with step 1 → **[Your first scalar function](scalar.md)**. diff --git a/docs/tutorial/scalar.md b/docs/tutorial/scalar.md new file mode 100644 index 0000000..3b9fab1 --- /dev/null +++ b/docs/tutorial/scalar.md @@ -0,0 +1,98 @@ +--- +description: "Tutorial step 1: build and run a scalar VGI function callable from DuckDB." +--- + +# 1. Your first scalar function + +**What this is:** the first tutorial step — build a worker with one **scalar** function and call +it from SQL.
+**Who it's for:** first-time VGI users who've met the +[prerequisites](index.md#prerequisites). About **10 minutes**. + +A scalar function maps one row to one row: `double(21)` → `42`. + +## Step 1 — Write the worker + +Create a file called `calc_scalar_worker.py`: + +```python +--8<-- "examples/calc_scalar_worker.py" +``` + +What's going on: + +1. **The `# /// script` header** is [inline script + metadata](https://packaging.python.org/en/latest/specifications/inline-script-metadata/) — it + tells `uv run` which dependencies to provision, so there's no virtualenv to create and nothing + to `pip install` first. +2. **`Double.compute` receives a whole column** (`pa.Int64Array`) and returns a column of the same + length. The `Annotated[..., Param(...)]` and `Annotated[..., Returns()]` types *are* the + schema — VGI derives the SQL signature from them. +3. **`CalcWorker` exposes a catalog** named `calc` containing the function. + +??? info "Why a column instead of a single value?" + VGI hands your function a batch of rows as an Arrow array, not one value at a time. Operating + on the whole column with `pyarrow.compute` (here `pc.multiply`) is what keeps it fast. If + you've written a DuckDB UDF before, this is the vectorized equivalent. + +## Step 2 — Launch a SQL engine and attach the worker + +=== "Haybarn (recommended)" + + [Haybarn](https://github.com/Query-farm-haybarn/haybarn) is Query.Farm's DuckDB distribution. + It ships the `vgi` extension and runs with no install via `uvx`: + + ```bash + uvx haybarn-cli + ``` + + At the prompt, attach your worker. `LOCATION` is the command Haybarn runs to launch it: + + ```sql + ATTACH 'calc' (TYPE vgi, LOCATION 'uv run calc_scalar_worker.py'); + ``` + +=== "Stock DuckDB" + + With stock [DuckDB](https://duckdb.org/), load the `vgi` extension from the community + repository first: + + ```sql + INSTALL vgi FROM community; + LOAD vgi; + ATTACH 'calc' (TYPE vgi, LOCATION 'uv run calc_scalar_worker.py'); + ``` + +## Step 3 — Call your function + +```sql +SELECT calc.double(21); +-- ┌──────────────────┐ +-- │ double(21) │ +-- │ int64 │ +-- ├──────────────────┤ +-- │ 42 │ +-- └──────────────────┘ +``` + +Over a real column: + +```sql +SELECT calc.double(n) FROM (VALUES (1), (2), (3)) AS t(n); +-- 2, 4, 6 +``` + +You've built and run your first VGI function. 🎉 + +??? success "It didn't work?" + - **`Catalog Error: unknown type "vgi"`** — the extension isn't loaded. On stock DuckDB run + `INSTALL vgi FROM community; LOAD vgi;` first; on Haybarn it's built in. + - **The `ATTACH` hangs or errors immediately** — run `uv run calc_scalar_worker.py` directly in + a terminal. The worker speaks Arrow over stdin/stdout, so it *looks* like it hangs waiting for + input — that's expected. You're checking for an import error or traceback on stderr. + - **`Binder Error: function not found`** — the SQL name is the snake_case of the class name + (`Double` → `double`), qualified by the catalog name from `ATTACH`. + +## Next steps + +- **[2. Add a table function](table.md)** — generate rows from an argument. diff --git a/docs/tutorial/table.md b/docs/tutorial/table.md new file mode 100644 index 0000000..ac35da0 --- /dev/null +++ b/docs/tutorial/table.md @@ -0,0 +1,84 @@ +--- +description: "Tutorial step 2: add a table function that generates rows, callable from DuckDB." +--- + +# 2. Add a table function + +**What this is:** the second tutorial step — extend your worker with a **table** function that +generates rows.
+**Who it's for:** anyone who's finished +[step 1](scalar.md). About **10 minutes**. + +A table function produces rows from its arguments (no input table): +`SELECT * FROM series(3)` returns three rows. + +## Step 1 — Extend the worker + +Update your worker to add a `Series` table function. The full file — scalar function unchanged, +table function added — is below. Save it as `calc_worker.py`: + +```python +--8<-- "examples/calc_worker.py" +``` + +The new pieces, compared to step 1: + +1. **`SeriesArgs`** — a typed arguments dataclass. `Arg(0, ...)` makes `count` the first positional + SQL argument. +2. **`Series`** — the generator. `FIXED_SCHEMA` declares its output columns; `process` emits the + rows with `out.emit(...)` and signals completion with `out.finish()`. Here it emits everything + in one call — no state to track. The `@bind_fixed_schema` and `@init_single_worker` decorators + wire up the bind/init lifecycle for the common single-worker case. + +??? info "Scalar vs. table — when do I use which?" + Use a **scalar** function when output has exactly one row per input row (a transform). Use a + **table** function when you generate rows independent of any input — a sequence, a data source, + an API result set. There are three more patterns (table-in-out, aggregate, and buffering) + covered in the [how-to guides](../how-to/function-patterns.md). + +??? info "Generating a lot of rows? Stream with state" + `process` is actually called *repeatedly* until you call `out.finish()`. For large results you + don't build one giant batch — you emit a bounded chunk per call and remember your place in a + small **state** object. That's the next thing to learn: + [streaming with state](../how-to/function-patterns.md#streaming-with-state). + +## Step 2 — Attach and call it + +Re-attach the updated worker, then call both functions: + +```sql +ATTACH 'calc' (TYPE vgi, LOCATION 'uv run calc_worker.py'); + +-- The scalar from step 1 still works: +SELECT calc.double(21); + +-- The new table function generates rows: +SELECT * FROM calc.series(3); +-- ┌─────┐ +-- │ n │ +-- ├─────┤ +-- │ 0 │ +-- │ 1 │ +-- │ 2 │ +-- └─────┘ +``` + +That's both function patterns running from SQL. 🎉 + +??? success "It didn't work?" + - **`Binder Error: table function ... does not exist`** — the SQL name is the snake_case of the + class name (`Series` → `series`) and a table function is called in `FROM`, not `SELECT`: + `SELECT * FROM calc.series(3)`, not `SELECT calc.series(3)`. + - **The query hangs and never returns** — a table generator must call `out.finish()` when it has + no more rows. Without it the framework keeps calling `process` forever. + - **`ATTACH` errors after editing** — if `calc` is already attached from step 1, `DETACH calc;` + first (or attach under a new name). + +## Next steps + +- **More function patterns** → [How-to: function patterns](../how-to/function-patterns.md) covers + table-in-out (streaming transforms), aggregates, buffering, and a string-valued scalar. +- **Understand what just happened** → [Concepts: worker lifecycle](../concepts/index.md) explains + bind → init → process → finish and the transports. +- **Look up the exact API** → the [API Reference](../api/index.md) documents every class and + argument type. diff --git a/examples/calc_scalar_worker.py b/examples/calc_scalar_worker.py new file mode 100644 index 0000000..94ac8b3 --- /dev/null +++ b/examples/calc_scalar_worker.py @@ -0,0 +1,45 @@ +# /// script +# requires-python = ">=3.13" +# dependencies = ["vgi-python"] +# /// +"""Stage 1 of the tutorial: a worker with a single scalar function. + +Run it from a DuckDB-compatible engine (Haybarn shown here):: + + uvx haybarn-cli + ATTACH 'calc' (TYPE vgi, LOCATION 'uv run calc_scalar_worker.py'); + SELECT calc.double(21); -- 42 +""" + +from typing import Annotated + +import pyarrow as pa +import pyarrow.compute as pc + +from vgi import Param, Returns, ScalarFunction, Worker +from vgi.catalog import Catalog, Schema + + +class Double(ScalarFunction): + """Double each input value (one row in, one row out).""" + + @classmethod + def compute( + cls, + value: Annotated[pa.Int64Array, Param(doc="Values to double")], + ) -> Annotated[pa.Int64Array, Returns()]: + """Multiply the whole column by 2.""" + return pc.multiply(value, 2) + + +class CalcWorker(Worker): + """A worker exposing the ``calc`` catalog with one scalar function.""" + + catalog = Catalog( + name="calc", + schemas=[Schema(name="main", functions=[Double])], + ) + + +if __name__ == "__main__": + CalcWorker().run() diff --git a/examples/calc_worker.py b/examples/calc_worker.py new file mode 100644 index 0000000..d75ee0f --- /dev/null +++ b/examples/calc_worker.py @@ -0,0 +1,88 @@ +# /// script +# requires-python = ">=3.13" +# dependencies = ["vgi-python"] +# /// +"""The full tutorial worker: a scalar function and a table function. + +Run from a DuckDB-compatible engine (Haybarn shown here):: + + uvx haybarn-cli + ATTACH 'calc' (TYPE vgi, LOCATION 'uv run calc_worker.py'); + SELECT calc.double(21); -- scalar -> 42 + SELECT * FROM calc.series(3); -- table -> 0, 1, 2 +""" + +from dataclasses import dataclass +from typing import Annotated, ClassVar + +import pyarrow as pa +import pyarrow.compute as pc + +from vgi import Arg, Param, Returns, ScalarFunction, Worker +from vgi.catalog import Catalog, Schema +from vgi.table_function import ( + OutputCollector, + ProcessParams, + TableFunctionGenerator, + bind_fixed_schema, + init_single_worker, +) + + +# --8<-- [start:scalar] +class Double(ScalarFunction): + """Double each input value (one row in, one row out).""" + + @classmethod + def compute( + cls, + value: Annotated[pa.Int64Array, Param(doc="Values to double")], + ) -> Annotated[pa.Int64Array, Returns()]: + """Multiply the whole column by 2.""" + return pc.multiply(value, 2) + + +# --8<-- [end:scalar] + + +# --8<-- [start:table] +@dataclass(slots=True, frozen=True, kw_only=True) +class SeriesArgs: + """Arguments for :class:`Series` (one positional ``count``).""" + + count: Annotated[int, Arg(0, doc="How many numbers to generate", ge=0)] + + +@init_single_worker +@bind_fixed_schema +class Series(TableFunctionGenerator[SeriesArgs]): + """Generate the integers ``0 .. count-1`` as a one-column table. + + Stateless: it emits every row in a single ``process`` call, then finishes. + ``@bind_fixed_schema`` derives the output schema from ``FIXED_SCHEMA``; + ``@init_single_worker`` runs the generator in a single worker. + """ + + FIXED_SCHEMA: ClassVar[pa.Schema] = pa.schema([("n", pa.int64())]) + + @classmethod + def process(cls, params: ProcessParams[SeriesArgs], state: None, out: OutputCollector) -> None: + """Emit all rows at once, then signal completion.""" + out.emit(pa.RecordBatch.from_pydict({"n": list(range(params.args.count))}, schema=params.output_schema)) + out.finish() + + +# --8<-- [end:table] + + +class CalcWorker(Worker): + """A worker exposing the ``calc`` catalog with both functions.""" + + catalog = Catalog( + name="calc", + schemas=[Schema(name="main", functions=[Double, Series])], + ) + + +if __name__ == "__main__": + CalcWorker().run() diff --git a/examples/filter_worker.py b/examples/filter_worker.py new file mode 100644 index 0000000..c2e478d --- /dev/null +++ b/examples/filter_worker.py @@ -0,0 +1,67 @@ +# /// script +# requires-python = ">=3.13" +# dependencies = ["vgi-python"] +# /// +"""A table-in-out function: keep only rows where ``value`` is positive. + +Table-in-out functions stream an input table through, batch by batch, emitting +transformed output. Run from a DuckDB-compatible engine:: + + ATTACH 'filters' (TYPE vgi, LOCATION 'uv run filter_worker.py'); + SELECT * FROM filters.filter_positive((SELECT * FROM my_table)); +""" + +from dataclasses import dataclass +from typing import Annotated + +import pyarrow as pa +import pyarrow.compute as pc + +from vgi import Arg, Worker +from vgi.arguments import TableInput +from vgi.catalog import Catalog, Schema +from vgi.invocation import BindResponse +from vgi.table_function import BindParams, ProcessParams +from vgi.table_in_out_function import OutputCollector, TableInOutGenerator + + +@dataclass(slots=True, frozen=True, kw_only=True) +class FilterArgs: + """Arguments: a single input table.""" + + data: Annotated[TableInput, Arg(0, doc="Input table to filter")] + + +class FilterPositive(TableInOutGenerator[FilterArgs]): + """Emit only the input rows whose ``value`` column is greater than zero.""" + + @classmethod + def on_bind(cls, params: BindParams[FilterArgs]) -> BindResponse: + """Output schema equals the input schema (rows are filtered, not reshaped).""" + assert params.bind_call.input_schema is not None + return BindResponse(output_schema=params.bind_call.input_schema) + + @classmethod + def process( + cls, + params: ProcessParams[FilterArgs], + state: None, + batch: pa.RecordBatch, + out: OutputCollector, + ) -> None: + """Filter each input batch and emit the surviving rows.""" + mask = pc.greater(batch.column("value"), pa.scalar(0, type=batch.column("value").type)) + out.emit(batch.filter(mask)) + + +class FilterWorker(Worker): + """A worker exposing the ``filters`` catalog.""" + + catalog = Catalog( + name="filters", + schemas=[Schema(name="main", functions=[FilterPositive])], + ) + + +if __name__ == "__main__": + FilterWorker().run() diff --git a/examples/greeting_scalar_worker.py b/examples/greeting_scalar_worker.py new file mode 100644 index 0000000..4bcf1ce --- /dev/null +++ b/examples/greeting_scalar_worker.py @@ -0,0 +1,45 @@ +# /// script +# requires-python = ">=3.13" +# dependencies = ["vgi-python"] +# /// +"""Stage 1 of the tutorial: a worker with a single scalar function. + +Run it from a DuckDB-compatible engine (Haybarn shown here):: + + uvx haybarn-cli + ATTACH 'greetings' (TYPE vgi, LOCATION 'uv run greeting_scalar_worker.py'); + SELECT greetings.greeting('Alice'); +""" + +from typing import Annotated + +import pyarrow as pa +import pyarrow.compute as pc + +from vgi import Param, Returns, ScalarFunction, Worker +from vgi.catalog import Catalog, Schema + + +class Greeting(ScalarFunction): + """Return a friendly greeting for each name (one row in, one row out).""" + + @classmethod + def compute( + cls, + name: Annotated[pa.StringArray, Param(doc="Column of names to greet")], + ) -> Annotated[pa.StringArray, Returns()]: + """Join ``Hello, `` + name + ``!`` element-wise across the column.""" + return pc.binary_join_element_wise("Hello, ", name, "!", "") + + +class GreetingWorker(Worker): + """A worker exposing the ``greetings`` catalog with one scalar function.""" + + catalog = Catalog( + name="greetings", + schemas=[Schema(name="main", functions=[Greeting])], + ) + + +if __name__ == "__main__": + GreetingWorker().run() diff --git a/examples/row_count_worker.py b/examples/row_count_worker.py new file mode 100644 index 0000000..844ef65 --- /dev/null +++ b/examples/row_count_worker.py @@ -0,0 +1,111 @@ +# /// script +# requires-python = ">=3.13" +# dependencies = ["vgi-python"] +# /// +"""A buffering function: count every input row, then emit one total. + +A buffering function must see the *whole* input before it can produce output — +the basis for sorts, top-k, and full-stream reductions. It runs in three phases: + +- **sink** (``process``): called per input batch; stash a partial in shared + storage and return a ``state_id``. +- **combine**: called once after all input; reduce the partials into a result. +- **source** (``finalize``): called per tick to stream the result out. + +State crosses process boundaries, so it lives in ``params.storage`` (scoped by +``execution_id``), not in memory. + + ATTACH 'buffers' (TYPE vgi, LOCATION 'uv run row_count_worker.py'); + SELECT * FROM buffers.row_count((SELECT * FROM big_table)); +""" + +from dataclasses import dataclass +from typing import Annotated + +import pyarrow as pa +from vgi_rpc import ArrowSerializableDataclass + +from vgi import Arg, Worker +from vgi.arguments import TableInput +from vgi.catalog import Catalog, Schema +from vgi.invocation import BindResponse +from vgi.table_buffering_function import OutputCollector, TableBufferingFunction, TableBufferingParams +from vgi.table_function import BindParams + +_RESULT = pa.schema([("count", pa.int64())]) + + +@dataclass(slots=True, frozen=True, kw_only=True) +class RowCountArgs: + """Arguments: a single input table to count.""" + + data: Annotated[TableInput, Arg(0, doc="Input table")] + + +@dataclass(kw_only=True) +class DrainState(ArrowSerializableDataclass): + """Per-finalize-stream cursor: emit the total once, then finish.""" + + done: bool = False + + +class RowCount(TableBufferingFunction[RowCountArgs, DrainState]): + """Count all input rows and emit a single ``count`` row.""" + + class Meta: + """Function metadata.""" + + name = "row_count" + + @classmethod + def on_bind(cls, params: BindParams[RowCountArgs]) -> BindResponse: + """Output is one int64 column regardless of input shape.""" + return BindResponse(output_schema=_RESULT) + + @classmethod + def process(cls, batch: pa.RecordBatch, params: TableBufferingParams[RowCountArgs]) -> bytes: + """Sink: stash this batch's row count; one bucket per execution.""" + params.storage.state_append(b"counts", b"", batch.num_rows.to_bytes(8, "little")) + return params.execution_id + + @classmethod + def combine(cls, state_ids: list[bytes], params: TableBufferingParams[RowCountArgs]) -> list[bytes]: + """Combine: sum the partial counts into a single result.""" + total = sum(int.from_bytes(v, "little") for _id, v in params.storage.state_log_scan(b"counts", b"")) + params.storage.state_append(b"result", b"", total.to_bytes(8, "little")) + return [params.execution_id] + + @classmethod + def initial_finalize_state(cls, finalize_state_id: bytes, params: TableBufferingParams[RowCountArgs]) -> DrainState: + """One cursor per finalize stream.""" + return DrainState() + + @classmethod + def finalize( + cls, + params: TableBufferingParams[RowCountArgs], + finalize_state_id: bytes, + state: DrainState, + out: OutputCollector, + ) -> None: + """Source: emit the total once, then signal completion.""" + if state.done: + out.finish() + return + rows = params.storage.state_log_scan(b"result", b"") + total = int.from_bytes(rows[-1][1], "little") if rows else 0 + out.emit(pa.RecordBatch.from_pydict({"count": [total]}, schema=params.output_schema)) + state.done = True + + +class BufferWorker(Worker): + """A worker exposing the ``buffers`` catalog.""" + + catalog = Catalog( + name="buffers", + schemas=[Schema(name="main", functions=[RowCount])], + ) + + +if __name__ == "__main__": + BufferWorker().run() diff --git a/examples/series_streaming_worker.py b/examples/series_streaming_worker.py new file mode 100644 index 0000000..59de020 --- /dev/null +++ b/examples/series_streaming_worker.py @@ -0,0 +1,86 @@ +# /// script +# requires-python = ">=3.13" +# dependencies = ["vgi-python"] +# /// +"""A table function that streams its output in chunks, using generator state. + +The tutorial's ``series`` emits everything in one call. When the output is large +you instead emit a bounded batch per ``process`` call and remember your place in +**state** — the framework calls ``process`` repeatedly until you ``out.finish()``. + + ATTACH 'calc' (TYPE vgi, LOCATION 'uv run series_streaming_worker.py'); + SELECT * FROM calc.series(1000000); -- streamed 10k rows at a time +""" + +from dataclasses import dataclass +from typing import Annotated, ClassVar + +import pyarrow as pa +from vgi_rpc import ArrowSerializableDataclass + +from vgi import Arg, Worker +from vgi.catalog import Catalog, Schema +from vgi.table_function import ( + OutputCollector, + ProcessParams, + TableFunctionGenerator, + bind_fixed_schema, + init_single_worker, +) + +CHUNK = 10_000 + + +@dataclass(slots=True, frozen=True, kw_only=True) +class SeriesArgs: + """Arguments for :class:`Series` (one positional ``count``).""" + + count: Annotated[int, Arg(0, doc="How many numbers to generate", ge=0)] + + +@dataclass(kw_only=True) +class SeriesState(ArrowSerializableDataclass): + """Cursor remembering how many rows we've emitted across ``process`` calls. + + Extends ``ArrowSerializableDataclass`` so the cursor survives HTTP state + round-trips (the framework requires serializable state for generators). + """ + + emitted: int = 0 + + +@init_single_worker +@bind_fixed_schema +class Series(TableFunctionGenerator[SeriesArgs, SeriesState]): + """Generate ``0 .. count-1`` in chunks, keeping a cursor in state.""" + + FIXED_SCHEMA: ClassVar[pa.Schema] = pa.schema([("n", pa.int64())]) + + @classmethod + def initial_state(cls, params: ProcessParams[SeriesArgs]) -> SeriesState: + """Start a fresh cursor at zero.""" + return SeriesState() + + @classmethod + def process(cls, params: ProcessParams[SeriesArgs], state: SeriesState, out: OutputCollector) -> None: + """Emit one bounded chunk per call; finish when the cursor reaches count.""" + if state.emitted >= params.args.count: + out.finish() + return + batch_size = min(params.args.count - state.emitted, CHUNK) + values = list(range(state.emitted, state.emitted + batch_size)) + out.emit(pa.RecordBatch.from_pydict({"n": values}, schema=params.output_schema)) + state.emitted += batch_size + + +class CalcWorker(Worker): + """A worker exposing the ``calc`` catalog with the streaming series.""" + + catalog = Catalog( + name="calc", + schemas=[Schema(name="main", functions=[Series])], + ) + + +if __name__ == "__main__": + CalcWorker().run() diff --git a/examples/sum_worker.py b/examples/sum_worker.py new file mode 100644 index 0000000..acfa3c2 --- /dev/null +++ b/examples/sum_worker.py @@ -0,0 +1,101 @@ +# /// script +# requires-python = ">=3.13" +# dependencies = ["vgi-python"] +# /// +"""An aggregate function: sum a column per group. + +Aggregate functions accumulate input rows into per-group state, then emit one +result row per group. They are driven by DuckDB's ``GROUP BY``:: + + ATTACH 'aggregates' (TYPE vgi, LOCATION 'uv run sum_worker.py'); + SELECT category, aggregates.vgi_sum(value) FROM t GROUP BY category; + +The three phases: + +- ``update`` — fold a batch of values into per-group state (keyed by group id) +- ``combine`` — merge two partial states for the same group (parallel workers) +- ``finalize`` — turn each group's state into its output row +""" + +from dataclasses import dataclass +from typing import Annotated + +import pyarrow as pa +from vgi_rpc import ArrowSerializableDataclass, ArrowType + +from vgi import Worker +from vgi.aggregate_function import AggregateFunction +from vgi.arguments import Param, Returns +from vgi.catalog import Catalog, Schema +from vgi.metadata import DistinctDependence, NullHandling, OrderDependence +from vgi.table_function import ProcessParams + + +@dataclass(kw_only=True) +class SumState(ArrowSerializableDataclass): + """Running total for one group. Serializable so it survives parallel combine.""" + + total: Annotated[int, ArrowType(pa.int64())] = 0 + + +class Sum(AggregateFunction[SumState]): + """Sum an int64 column, grouped by DuckDB's ``GROUP BY`` columns.""" + + class Meta: + """Function metadata: name, description, and aggregate semantics.""" + + name = "vgi_sum" + description = "Sum integer values per group" + null_handling = NullHandling.DEFAULT + order_dependent = OrderDependence.NOT_ORDER_DEPENDENT + distinct_dependent = DistinctDependence.NOT_DISTINCT_DEPENDENT + + @classmethod + def initial_state(cls, params: ProcessParams[None]) -> SumState: + """One fresh accumulator per group.""" + return SumState() + + @classmethod + def update( + cls, + states: dict[int, SumState], + group_ids: pa.Int64Array, + value: Annotated[pa.Int64Array, Param(doc="Column to sum")], + ) -> None: + """Fold a batch of values into each group's running total.""" + table = pa.table({"gid": group_ids, "value": value}) + grouped = table.group_by("gid").aggregate([("value", "sum")]) + for i in range(grouped.num_rows): + gid: int = grouped.column("gid")[i].as_py() + val = grouped.column("value_sum")[i].as_py() + if val is not None: + states[gid] = SumState(total=states[gid].total + val) + + @classmethod + def combine(cls, source: SumState, target: SumState, params: ProcessParams[None]) -> SumState: + """Merge two partial sums for the same group.""" + return SumState(total=source.total + target.total) + + @classmethod + def finalize( + cls, + group_ids: pa.Int64Array, + states: dict[int, SumState], + params: ProcessParams[None], + ) -> Annotated[pa.RecordBatch, Returns(pa.int64())]: + """Emit one total per group.""" + results = [s.total if (s := states[gid.as_py()]) is not None else None for gid in group_ids] + return pa.record_batch({"result": pa.array(results, type=pa.int64())}) + + +class AggregateWorker(Worker): + """A worker exposing the ``aggregates`` catalog.""" + + catalog = Catalog( + name="aggregates", + schemas=[Schema(name="main", functions=[Sum])], + ) + + +if __name__ == "__main__": + AggregateWorker().run() diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 0000000..c382fc6 --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,155 @@ +site_name: vgi-python +site_description: "vgi-python: extend DuckDB with functions written in Python (or any language) over Apache Arrow IPC — scalar, table, table-in-out, and aggregate functions, catalogs, and stateful workers" +site_url: https://vgi-python.query.farm/ +repo_url: https://github.com/Query-farm/vgi-python +repo_name: Query-farm/vgi-python +copyright: "© 2026 Query.Farm LLC" + +extra: + social: + - icon: material/web + link: https://query.farm + name: 🚜 Query.Farm + - icon: material/arrow-right-bold-circle + link: https://vgi-rpc-python.query.farm/ + name: vgi-rpc (RPC layer) + - icon: fontawesome/brands/github + link: https://github.com/Query-farm + name: 🚜 Query.Farm on GitHub + +extra_css: + - stylesheets/extra.css + +theme: + name: material + palette: + - media: "(prefers-color-scheme: light)" + scheme: default + primary: custom + accent: custom + toggle: + icon: material/weather-sunny + name: Switch to dark mode + - media: "(prefers-color-scheme: dark)" + scheme: slate + primary: custom + accent: custom + toggle: + icon: material/weather-night + name: Switch to light mode + features: + - navigation.tabs + - navigation.tabs.sticky + - navigation.sections + - navigation.indexes + - navigation.top + - navigation.footer + - content.code.copy + - search.highlight + - search.suggest + - toc.follow + font: + text: Inter + code: JetBrains Mono + custom_dir: docs/overrides + logo: assets/logo.png + favicon: assets/favicon.ico + icon: + repo: fontawesome/brands/github + +markdown_extensions: + - toc: + permalink: true + toc_depth: 3 + - tables + - admonition + - pymdownx.details + - pymdownx.superfences: + custom_fences: + - name: mermaid + class: mermaid + format: !!python/name:pymdownx.superfences.fence_code_format + - pymdownx.highlight: + anchor_linenums: true + - pymdownx.inlinehilite + - pymdownx.tabbed: + alternate_style: true + - pymdownx.snippets: + base_path: . + - attr_list + - md_in_html + +plugins: + - search + - section-index + - d2: + layout: dagre + theme: 104 + dark_theme: 200 + pad: 100 + cache: true + - mkdocstrings: + default_handler: python + handlers: + python: + paths: [.] + options: + show_source: true + show_bases: true + show_root_heading: true + show_root_full_path: false + show_symbol_type_heading: true + show_symbol_type_toc: false + heading_level: 2 + members_order: source + show_if_no_docstring: false + docstring_style: google + docstring_section_style: spacy + merge_init_into_class: true + show_signature: true + show_signature_annotations: true + separate_signature: true + signature_crossrefs: true + +nav: + - Home: index.md + - Tutorial: + - tutorial/index.md + - 1. Scalar function: tutorial/scalar.md + - 2. Table function: tutorial/table.md + - How-to guides: + - how-to/index.md + - Function patterns: how-to/function-patterns.md + - Expose a catalog: how-to/catalogs.md + - Persist state across workers: how-to/state-storage.md + - Serve over HTTP with auth: how-to/http-auth.md + - Integrate with the optimizer: how-to/pushdown-and-statistics.md + - Function Metadata: metadata.md + - CLI: cli.md + - "Reference: Function API": generator-api.md + - "Reference: Aggregate Functions": aggregate-functions.md + - "Reference: Catalog Interface": catalog-interface.md + - "Reference: Shared Storage": shared-storage.md + - "Reference: Authentication": authentication.md + - "Reference: Filter Pushdown": filter-pushdown.md + - "Reference: Column Statistics": column-statistics.md + - Concepts: + - concepts/index.md + - Function Lifecycle: lifecycle.md + - Argument Serialization: argument-serialization.md + - API Reference: + - api/index.md + - Functions: api/functions.md + - Arguments & Schema: api/arguments.md + - Worker & Serving: api/worker.md + - Client: api/client.md + - Catalogs: api/catalogs.md + - State Storage: api/storage.md + - Metadata & Protocol: api/metadata.md + - Filter Pushdown: api/filters.md + - Auth & Secrets: api/auth.md + - Observability: api/observability.md + - HTTP: api/http.md + - Transactor: api/transactor.md + - Exceptions: api/exceptions.md + - Contributing to docs: contributing-docs.md diff --git a/pyproject.toml b/pyproject.toml index 29347f8..d87b6cc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -85,7 +85,7 @@ vgi-transactor = "vgi.transactor.server:main" [project.urls] Homepage = "https://query.farm" Repository = "https://github.com/Query-farm/vgi-python" -Documentation = "https://github.com/Query-farm/vgi-python/tree/main/docs" +Documentation = "https://vgi-python.query.farm/" Issues = "https://github.com/Query-farm/vgi-python/issues" [build-system] @@ -188,3 +188,13 @@ vgi-fixtures = { workspace = true } [dependency-groups] dev = ["lxml>=6.0.2", "pytest-timeout>=2.4.0", "ty>=0.0.8", "vgi-fixtures"] +docs = [ + "mkdocs>=1.6", + "mkdocs-material>=9.5", + "mkdocstrings[python]>=0.27", + "mkdocs-section-index>=0.3", + "mkdocs-d2-plugin>=1.4", + "pygments>=2.20", + "pymdown-extensions>=10.21.2", + "ruff>=0.1", +] diff --git a/tests/test_documentation_examples.py b/tests/test_documentation_examples.py index f25328c..ac36a7b 100644 --- a/tests/test_documentation_examples.py +++ b/tests/test_documentation_examples.py @@ -77,7 +77,11 @@ "OrderPreservation": OrderPreservation, } -# All documentation files to test +# All documentation files to test. Directories are recursed by find_examples, so +# the Diátaxis trees (tutorial/how-to/concepts) are covered wholesale. Worker code +# is embedded in those pages via pymdownx snippets (``--8<--``); the snippet line +# itself is skipped here and the embedded files are exercised directly by +# test_examples_workers.py. DOC_FILES = [ "CLAUDE.md", "README.md", @@ -87,6 +91,10 @@ "docs/lifecycle.md", "docs/metadata.md", "docs/cli.md", + "docs/contributing-docs.md", + "docs/tutorial", + "docs/how-to", + "docs/concepts", ] @@ -103,15 +111,24 @@ def _should_skip(example: CodeExample) -> bool: if settings.get("test") == "skip": return True + source = example.source + + # Render-safe skip sentinel. pymdownx.superfences mishandles the + # ``test="skip"`` info string (it leaks the fence delimiters into the page), + # so illustrative sketches use a plain ```python fence plus a leading + # ``# illustrative`` comment to opt out of linting/execution here. + if "# illustrative" in source: + return True + # Skip examples with intentionally invalid syntax # These are partial snippets that can't even be linted - source = example.source invalid_syntax_markers = [ '"settings": (', # Dict entry without dict context "attach_opaque_data=...,", # Placeholder arguments with trailing comma "# ... implement", # Placeholder comment instead of implementation "...,", # Ellipsis with comma (placeholder in arg list) ") -> ...:", # Ellipsis as return type placeholder + "--8<--", # pymdownx snippet directive — the embedded file is tested directly ] return any(marker in source for marker in invalid_syntax_markers) diff --git a/tests/test_examples_workers.py b/tests/test_examples_workers.py new file mode 100644 index 0000000..496005d --- /dev/null +++ b/tests/test_examples_workers.py @@ -0,0 +1,133 @@ +# Copyright 2025, 2026 Query Farm LLC - https://query.farm + +"""End-to-end tests for the documentation example workers in ``examples/``. + +The docs embed these files via pymdownx snippets, so ``find_examples`` never +executes them. This module is the source of truth that they actually run: + +- every ``examples/*.py`` module imports cleanly, and +- the scalar / table / table-in-out workers serve real results over the + subprocess transport, and the aggregate worker accumulates correctly. + +Workers are spawned with the current interpreter (``sys.executable``) so the +already-installed ``vgi`` is used — no ``uv run`` dependency re-resolution. +""" + +from __future__ import annotations + +import sys +from pathlib import Path + +import pyarrow as pa +import pytest + +from vgi.client import Client + +EXAMPLES_DIR = Path(__file__).resolve().parent.parent / "examples" + +# Make the example modules importable by name (the framework re-imports a +# function's defining module during __init_subclass__). +if str(EXAMPLES_DIR) not in sys.path: + sys.path.insert(0, str(EXAMPLES_DIR)) + +ALL_EXAMPLES = sorted(p.name for p in EXAMPLES_DIR.glob("*.py")) + + +def _spawn(script: str) -> Client: + """Return a subprocess Client for an example worker, run with this interpreter.""" + return Client(f"{sys.executable} {EXAMPLES_DIR / script}", pool=None) + + +@pytest.mark.parametrize("filename", ALL_EXAMPLES, ids=ALL_EXAMPLES) +def test_example_imports(filename: str) -> None: + """Every example module imports without error.""" + __import__(filename.removesuffix(".py")) + + +def test_calc_scalar_worker() -> None: + """The stage-1 scalar-only tutorial worker doubles each input row.""" + with _spawn("calc_scalar_worker.py") as client: + batch = pa.record_batch({"value": pa.array([21, 5], type=pa.int64())}) + out = list(client.scalar_function(function_name="double", input=iter([batch]))) + assert [v for b in out for v in b.column(0).to_pylist()] == [42, 10] + + +def test_calc_worker_scalar_and_table() -> None: + """The full tutorial worker serves both the scalar and the table function.""" + with _spawn("calc_worker.py") as client: + values = pa.record_batch({"value": pa.array([21], type=pa.int64())}) + scalar = list(client.scalar_function(function_name="double", input=iter([values]))) + assert scalar[0].column(0).to_pylist() == [42] + + with _spawn("calc_worker.py") as client: + from vgi.arguments import Arguments + + rows = list( + client.table_function( + function_name="series", + arguments=Arguments(positional=(pa.scalar(3),)), + ) + ) + assert [v for b in rows for v in b.column("n").to_pylist()] == [0, 1, 2] + + +def test_series_streaming_worker() -> None: + """The stateful streaming generator emits the full range across chunked calls.""" + from vgi.arguments import Arguments + + with _spawn("series_streaming_worker.py") as client: + rows = list( + client.table_function( + function_name="series", + arguments=Arguments(positional=(pa.scalar(5),)), + ) + ) + assert [v for b in rows for v in b.column("n").to_pylist()] == [0, 1, 2, 3, 4] + + +def test_row_count_worker_buffering() -> None: + """The buffering worker counts every input row across batches and emits one total.""" + with _spawn("row_count_worker.py") as client: + batches = [ + pa.record_batch({"x": pa.array([1, 2, 3], type=pa.int64())}), + pa.record_batch({"x": pa.array([4, 5], type=pa.int64())}), + ] + out = list(client.table_buffering_function(function_name="row_count", input=iter(batches))) + assert [v for b in out for v in b.column("count").to_pylist()] == [5] + + +def test_greeting_scalar_worker_string_example() -> None: + """The string-scalar example (used in the function-patterns guide) still serves.""" + with _spawn("greeting_scalar_worker.py") as client: + batch = pa.record_batch({"name": pa.array(["Alice", "Bob"])}) + out = list(client.scalar_function(function_name="greeting", input=iter([batch]))) + assert [v for b in out for v in b.column(0).to_pylist()] == ["Hello, Alice!", "Hello, Bob!"] + + +def test_filter_worker_table_in_out() -> None: + """The table-in-out worker keeps only rows whose value is positive.""" + with _spawn("filter_worker.py") as client: + batch = pa.record_batch({"value": pa.array([-2, 5, 0, 9, -1], type=pa.int64())}) + out = list(client.table_in_out_function(function_name="filter_positive", input=iter([batch]))) + kept = [v for b in out for v in b.column("value").to_pylist()] + assert kept == [5, 9] + + +def test_sum_worker_aggregate_phases() -> None: + """The aggregate worker accumulates per-group totals through its phases. + + Aggregates are driven by DuckDB's GROUP BY (no direct Client entry point), + so we exercise update -> combine -> finalize directly. + """ + import sum_worker as m + + states: dict[int, object] = {0: m.SumState(), 1: m.SumState()} + m.Sum.update(states, pa.array([0, 0, 1, 1, 1], type=pa.int64()), pa.array([10, 5, 1, 2, 3], type=pa.int64())) + assert states[0].total == 15 # type: ignore[attr-defined] + assert states[1].total == 6 # type: ignore[attr-defined] + + merged = m.Sum.combine(m.SumState(total=15), m.SumState(total=100), params=None) + assert merged.total == 115 + + out = m.Sum.finalize(pa.array([0, 1], type=pa.int64()), states, params=None) + assert out.column("result").to_pylist() == [15, 6] diff --git a/uv.lock b/uv.lock index 6bc74cc..83b5b62 100644 --- a/uv.lock +++ b/uv.lock @@ -114,6 +114,15 @@ 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" }, ] +[[package]] +name = "annotated-types" +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" }, +] + [[package]] name = "anyio" version = "4.12.1" @@ -164,6 +173,28 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/49/9a/417b3a533e01953a7c618884df2cb05a71e7b68bdbce4fbdb62349d2a2e8/azure_identity-1.25.3-py3-none-any.whl", hash = "sha256:f4d0b956a8146f30333e071374171f3cfa7bdb8073adb8c3814b65567aa7447c", size = 192138, upload-time = "2026-03-13T01:12:22.951Z" }, ] +[[package]] +name = "babel" +version = "2.18.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/b2/51899539b6ceeeb420d40ed3cd4b7a40519404f9baf3d4ac99dc413a834b/babel-2.18.0.tar.gz", hash = "sha256:b80b99a14bd085fcacfa15c9165f651fbb3406e66cc603abf11c5750937c992d", size = 9959554, upload-time = "2026-02-01T12:30:56.078Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/f5/21d2de20e8b8b0408f0681956ca2c69f1320a3848ac50e6e7f39c6159675/babel-2.18.0-py3-none-any.whl", hash = "sha256:e2b422b277c2b9a9630c1d7903c2a00d0830c409c59ac8cae9081c92f1aeba35", size = 10196845, upload-time = "2026-02-01T12:30:53.445Z" }, +] + +[[package]] +name = "backrefs" +version = "7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5e/a7/a7dd63622beef68cc0d3c3c36d472e143dd95443d5ebf14cd1a5b4dfbf11/backrefs-7.0.tar.gz", hash = "sha256:4989bb9e1e99eb23647c7160ed51fb21d0b41b5d200f2d3017da41e023097e82", size = 7012453, upload-time = "2026-04-28T16:28:04.215Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d4/39/39a31d7eae729ea14ed10c3ccef79371197177b9355a86cb3525709e8502/backrefs-7.0-py310-none-any.whl", hash = "sha256:b57cd227ea556b0aed3dc9b8da4628db4eabc0402c6d7fcfc69283a93955f7e9", size = 380824, upload-time = "2026-04-28T16:27:55.647Z" }, + { url = "https://files.pythonhosted.org/packages/c9/b5/9302644225ba7dfa934a2ff2b9c7bb85701313a90dddb3dfaf693fa5bae2/backrefs-7.0-py311-none-any.whl", hash = "sha256:a0fa7360c63509e9e077e174ef4e6d3c21c8db94189b9d957289ae6d794b9475", size = 392626, upload-time = "2026-04-28T16:27:57.42Z" }, + { url = "https://files.pythonhosted.org/packages/36/da/87912ddec6e06feffbaa3d7aa18fc6352bee2e8f1fee185d7d1690f8f4e8/backrefs-7.0-py312-none-any.whl", hash = "sha256:ca42ce6a49ace3d75684dfa9937f3373902a63284ecb385ce36d15e5dcb41c12", size = 398537, upload-time = "2026-04-28T16:27:58.913Z" }, + { url = "https://files.pythonhosted.org/packages/00/bb/90ba423612b6aa0adccc6b1874bcd4a9b44b660c0c16f346611e00f64ac3/backrefs-7.0-py313-none-any.whl", hash = "sha256:f2c52955d631b9e1ac4cd56209f0a3a946d592b98e7790e77699339ae01c102a", size = 400491, upload-time = "2026-04-28T16:28:00.928Z" }, + { url = "https://files.pythonhosted.org/packages/3e/5c/fb93d3092640a24dfb7bd7727a24016d7c01774ca013e60efd3f683c8002/backrefs-7.0-py314-none-any.whl", hash = "sha256:a6448b28180e3ca01134c9cf09dcebafad8531072e09903c5451748a05f24bc9", size = 412349, upload-time = "2026-04-28T16:28:02.412Z" }, +] + [[package]] name = "black" version = "26.1.0" @@ -577,6 +608,27 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9a/9a/e35b4a917281c0b8419d4207f4334c8e8c5dbf4f3f5f9ada73958d937dcc/frozenlist-1.8.0-py3-none-any.whl", hash = "sha256:0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d", size = 13409, upload-time = "2025-10-06T05:38:16.721Z" }, ] +[[package]] +name = "ghp-import" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "python-dateutil" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d9/29/d40217cbe2f6b1359e00c6c307bb3fc876ba74068cbab3dde77f03ca0dc4/ghp-import-2.1.0.tar.gz", hash = "sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343", size = 10943, upload-time = "2022-05-02T15:47:16.11Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f7/ec/67fbef5d497f86283db54c22eec6f6140243aae73265799baaaa19cd17fb/ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619", size = 11034, upload-time = "2022-05-02T15:47:14.552Z" }, +] + +[[package]] +name = "griffelib" +version = "2.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9d/82/74f4a3310cdabfbb10da554c3a672847f1ed33c6f61dd472681ce7f1fe67/griffelib-2.0.2.tar.gz", hash = "sha256:3cf20b3bc470e83763ffbf236e0076b1211bac1bc67de13daf494640f2de707e", size = 166461, upload-time = "2026-03-27T11:34:51.091Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/11/8c/c9138d881c79aa0ea9ed83cbd58d5ca75624378b38cee225dcf5c42cc91f/griffelib-2.0.2-py3-none-any.whl", hash = "sha256:925c857658fb1ba40c0772c37acbc2ab650bd794d9c1b9726922e36ea4117ea1", size = 142357, upload-time = "2026-03-27T11:34:46.275Z" }, +] + [[package]] name = "h11" version = "0.16.0" @@ -666,6 +718,18 @@ 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" }, ] +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { 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" }, +] + [[package]] name = "joserfc" version = "1.6.4" @@ -808,6 +872,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/92/aa/df863bcc39c5e0946263454aba394de8a9084dbaff8ad143846b0d844739/lxml-6.0.2-cp314-cp314t-win_arm64.whl", hash = "sha256:bb4c1847b303835d89d785a18801a883436cdfd5dc3d62947f9c49e24f0f5a2c", size = 3822205, upload-time = "2025-09-22T04:03:36.249Z" }, ] +[[package]] +name = "markdown" +version = "3.10.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2b/f4/69fa6ed85ae003c2378ffa8f6d2e3234662abd02c10d216c0ba96081a238/markdown-3.10.2.tar.gz", hash = "sha256:994d51325d25ad8aa7ce4ebaec003febcce822c3f8c911e3b17c52f7f589f950", size = 368805, upload-time = "2026-02-09T14:57:26.942Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/1f/77fa3081e4f66ca3576c896ae5d31c3002ac6607f9747d2e3aa49227e464/markdown-3.10.2-py3-none-any.whl", hash = "sha256:e91464b71ae3ee7afd3017d9f358ef0baf158fd9a298db92f1d4761133824c36", size = 108180, upload-time = "2026-02-09T14:57:25.787Z" }, +] + [[package]] name = "markdown-it-py" version = "4.0.0" @@ -820,6 +893,58 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" }, ] +[[package]] +name = "markupsafe" +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" }, +] + [[package]] name = "mdurl" version = "0.1.2" @@ -829,6 +954,162 @@ 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" }, ] +[[package]] +name = "mergedeep" +version = "1.3.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3a/41/580bb4006e3ed0361b8151a01d324fb03f420815446c7def45d02f74c270/mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8", size = 4661, upload-time = "2021-02-05T18:55:30.623Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/19/04f9b178c2d8a15b076c8b5140708fa6ffc5601fb6f1e975537072df5b2a/mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307", size = 6354, upload-time = "2021-02-05T18:55:29.583Z" }, +] + +[[package]] +name = "mkdocs" +version = "1.6.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "ghp-import" }, + { name = "jinja2" }, + { name = "markdown" }, + { name = "markupsafe" }, + { name = "mergedeep" }, + { name = "mkdocs-get-deps" }, + { name = "packaging" }, + { name = "pathspec" }, + { name = "pyyaml" }, + { name = "pyyaml-env-tag" }, + { name = "watchdog" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bc/c6/bbd4f061bd16b378247f12953ffcb04786a618ce5e904b8c5a01a0309061/mkdocs-1.6.1.tar.gz", hash = "sha256:7b432f01d928c084353ab39c57282f29f92136665bdd6abf7c1ec8d822ef86f2", size = 3889159, upload-time = "2024-08-30T12:24:06.899Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/5b/dbc6a8cddc9cfa9c4971d59fb12bb8d42e161b7e7f8cc89e49137c5b279c/mkdocs-1.6.1-py3-none-any.whl", hash = "sha256:db91759624d1647f3f34aa0c3f327dd2601beae39a366d6e064c03468d35c20e", size = 3864451, upload-time = "2024-08-30T12:24:05.054Z" }, +] + +[[package]] +name = "mkdocs-autorefs" +version = "1.4.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown" }, + { name = "markupsafe" }, + { name = "mkdocs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/52/c0/f641843de3f612a6b48253f39244165acff36657a91cc903633d456ae1ac/mkdocs_autorefs-1.4.4.tar.gz", hash = "sha256:d54a284f27a7346b9c38f1f852177940c222da508e66edc816a0fa55fc6da197", size = 56588, upload-time = "2026-02-10T15:23:55.105Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/28/de/a3e710469772c6a89595fc52816da05c1e164b4c866a89e3cb82fb1b67c5/mkdocs_autorefs-1.4.4-py3-none-any.whl", hash = "sha256:834ef5408d827071ad1bc69e0f39704fa34c7fc05bc8e1c72b227dfdc5c76089", size = 25530, upload-time = "2026-02-10T15:23:53.817Z" }, +] + +[[package]] +name = "mkdocs-d2-plugin" +version = "1.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mkdocs" }, + { name = "packaging" }, + { name = "pydantic" }, + { name = "pymdown-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b3/26/17e92ae5f577eefe6fd9b15fb80e03c8e672b5477eb5501ce66277ea69a8/mkdocs_d2_plugin-1.7.0.tar.gz", hash = "sha256:607462a3bf3d28b0574214d400820f86c0c9f43466fdf73243e7f9f2e3d21584", size = 8373, upload-time = "2026-04-09T20:30:34.235Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f1/3b/002528206eb503287cb320ca9ac34f16159d41d9b77dd7ec594ebc2e4cae/mkdocs_d2_plugin-1.7.0-py3-none-any.whl", hash = "sha256:1c40092738c77d4b8b03880f01f4ef433de07fb8c4bbf9540819e161b6440e3f", size = 8789, upload-time = "2026-04-09T20:30:33.013Z" }, +] + +[[package]] +name = "mkdocs-get-deps" +version = "0.2.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mergedeep" }, + { name = "platformdirs" }, + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ce/25/b3cccb187655b9393572bde9b09261d267c3bf2f2cdabe347673be5976a6/mkdocs_get_deps-0.2.2.tar.gz", hash = "sha256:8ee8d5f316cdbbb2834bc1df6e69c08fe769a83e040060de26d3c19fad3599a1", size = 11047, upload-time = "2026-03-10T02:46:33.632Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/29/744136411e785c4b0b744d5413e56555265939ab3a104c6a4b719dad33fd/mkdocs_get_deps-0.2.2-py3-none-any.whl", hash = "sha256:e7878cbeac04860b8b5e0ca31d3abad3df9411a75a32cde82f8e44b6c16ff650", size = 9555, upload-time = "2026-03-10T02:46:32.256Z" }, +] + +[[package]] +name = "mkdocs-material" +version = "9.7.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "babel" }, + { name = "backrefs" }, + { name = "colorama" }, + { name = "jinja2" }, + { name = "markdown" }, + { name = "mkdocs" }, + { name = "mkdocs-material-extensions" }, + { name = "paginate" }, + { name = "pygments" }, + { name = "pymdown-extensions" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/45/29/6d2bcf41ae40802c4beda2432396fff97b8456fb496371d1bc7aad6512ec/mkdocs_material-9.7.6.tar.gz", hash = "sha256:00bdde50574f776d328b1862fe65daeaf581ec309bd150f7bff345a098c64a69", size = 4097959, upload-time = "2026-03-19T15:41:58.161Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/01/bc663630c510822c95c47a66af9fa7a443c295b47d5f041e5e6ae62ef659/mkdocs_material-9.7.6-py3-none-any.whl", hash = "sha256:71b84353921b8ea1ba84fe11c50912cc512da8fe0881038fcc9a0761c0e635ba", size = 9305470, upload-time = "2026-03-19T15:41:55.217Z" }, +] + +[[package]] +name = "mkdocs-material-extensions" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/79/9b/9b4c96d6593b2a541e1cb8b34899a6d021d208bb357042823d4d2cabdbe7/mkdocs_material_extensions-1.3.1.tar.gz", hash = "sha256:10c9511cea88f568257f960358a467d12b970e1f7b2c0e5fb2bb48cab1928443", size = 11847, upload-time = "2023-11-22T19:09:45.208Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5b/54/662a4743aa81d9582ee9339d4ffa3c8fd40a4965e033d77b9da9774d3960/mkdocs_material_extensions-1.3.1-py3-none-any.whl", hash = "sha256:adff8b62700b25cb77b53358dad940f3ef973dd6db797907c49e3c2ef3ab4e31", size = 8728, upload-time = "2023-11-22T19:09:43.465Z" }, +] + +[[package]] +name = "mkdocs-section-index" +version = "0.3.12" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mkdocs" }, + { name = "properdocs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f1/e2/64d0f3f054ca8efe61e706006ff5f0d49ad99620c62c2e04818573391c33/mkdocs_section_index-0.3.12.tar.gz", hash = "sha256:285635bf86c643b0fc7a343053d7a818049817bff4408f52b80c4367bd5e7268", size = 14946, upload-time = "2026-04-16T19:20:00.953Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b0/4d/a330cab5e055d45e924cec69da54a3d8ed37643964f8d1fa1a772b496273/mkdocs_section_index-0.3.12-py3-none-any.whl", hash = "sha256:a1100039546beb4ebef63ce6fc91f3195fb9c0c3763105d4d3d7cd31e0a046eb", size = 8932, upload-time = "2026-04-16T19:19:59.741Z" }, +] + +[[package]] +name = "mkdocstrings" +version = "1.0.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jinja2" }, + { name = "markdown" }, + { name = "markupsafe" }, + { name = "mkdocs" }, + { name = "mkdocs-autorefs" }, + { name = "pymdown-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1d/5d/f888d4d3eb31359b327bc9b17a212d6ef03fe0b0682fbb3fc2cb849fb12b/mkdocstrings-1.0.4.tar.gz", hash = "sha256:3969a6515b77db65fd097b53c1b7aa4ae840bd71a2ee62a6a3e89503446d7172", size = 100088, upload-time = "2026-04-15T09:16:53.376Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6e/94/be70f8ee9c45f2f62b39a1f0e9303bc20e138a8f3b8e50ffd89498e177e1/mkdocstrings-1.0.4-py3-none-any.whl", hash = "sha256:63464b4b29053514f32a1dbbf604e52876d5e638111b0c295ab7ed3cac73ca9b", size = 35560, upload-time = "2026-04-15T09:16:51.436Z" }, +] + +[package.optional-dependencies] +python = [ + { name = "mkdocstrings-python" }, +] + +[[package]] +name = "mkdocstrings-python" +version = "2.0.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "griffelib" }, + { name = "mkdocs-autorefs" }, + { name = "mkdocstrings" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a4/b4/5fed370d8ebd96e4e399460a7146ae989263f16588b05a6facd6dbd51e60/mkdocstrings_python-2.0.4.tar.gz", hash = "sha256:58c73c5d358e64e9b1673447663f4a2f8a8941e392e225fc0a0c893758cc452f", size = 199219, upload-time = "2026-06-05T08:13:01.819Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5e/e3/00ec594aef5f55522e6d373bc2ac53e53a8f5e9ae32f2d6854b0de4270f3/mkdocstrings_python-2.0.4-py3-none-any.whl", hash = "sha256:fd87c173e1e719a85997b6d4f852cdc55f36710e0ed08da3a7bd9abe79c9db00", size = 104790, upload-time = "2026-06-05T08:13:00.393Z" }, +] + [[package]] name = "msal" version = "1.36.0" @@ -1071,6 +1352,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" }, ] +[[package]] +name = "paginate" +version = "0.5.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ec/46/68dde5b6bc00c1296ec6466ab27dddede6aec9af1b99090e1107091b3b84/paginate-0.5.7.tar.gz", hash = "sha256:22bd083ab41e1a8b4f3690544afb2c60c25e5c9a63a30fa2f483f6c60c8e5945", size = 19252, upload-time = "2024-08-25T14:17:24.139Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/90/96/04b8e52da071d28f5e21a805b19cb9390aa17a47462ac87f5e2696b9566d/paginate-0.5.7-py2.py3-none-any.whl", hash = "sha256:b885e2af73abcf01d9559fd5216b57ef722f8c42affbb63942377668e35c7591", size = 13746, upload-time = "2024-08-25T14:17:22.55Z" }, +] + [[package]] name = "pathspec" version = "1.0.4" @@ -1167,6 +1457,29 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5b/5a/bc7b4a4ef808fa59a816c17b20c4bef6884daebbdf627ff2a161da67da19/propcache-0.4.1-py3-none-any.whl", hash = "sha256:af2a6052aeb6cf17d3e46ee169099044fd8224cbaf75c76a2ef596e8163e2237", size = 13305, upload-time = "2025-10-08T19:49:00.792Z" }, ] +[[package]] +name = "properdocs" +version = "1.6.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "ghp-import" }, + { name = "jinja2" }, + { name = "markdown" }, + { name = "markupsafe" }, + { name = "packaging" }, + { name = "pathspec" }, + { name = "platformdirs" }, + { name = "pyyaml" }, + { name = "pyyaml-env-tag" }, + { name = "watchdog" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ec/29/f27a4e1eddf72ed3db6e47818fbafe6debbf09fd7051f9c1a007239b46ef/properdocs-1.6.7.tar.gz", hash = "sha256:adc7b16e562890af0e098a7e5b02e3a81c20894a87d6a28d345c9300de73c26e", size = 276141, upload-time = "2026-03-20T20:07:48.167Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bd/4d/fc923f5c85318ee8cc903566dc4e0ebe41b2dfc1d2ecf5546db232397ed6/properdocs-1.6.7-py3-none-any.whl", hash = "sha256:6fa0cfa2e01bf338f684892c8a506cf70ea88ae7f3479c933b6fa20168101cbd", size = 225406, upload-time = "2026-03-20T20:07:46.875Z" }, +] + [[package]] name = "pyarrow" version = "23.0.0" @@ -1224,13 +1537,84 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" }, ] +[[package]] +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" }, +] +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" }, +] + +[[package]] +name = "pydantic-core" +version = "2.46.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { 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" }, +] + [[package]] name = "pygments" -version = "2.19.2" +version = "2.20.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +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/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, + { 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]] @@ -1247,6 +1631,19 @@ crypto = [ { name = "cryptography" }, ] +[[package]] +name = "pymdown-extensions" +version = "10.21.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown" }, + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9e/26/d1015444da4d952a1ca487a236b522eb979766f0295a0bd0c5fc089989a9/pymdown_extensions-10.21.3.tar.gz", hash = "sha256:72cfcf55f07aea0d4af2c4f11dd4e52466ddfb1bb819673146398e0bd3a77354", size = 854140, upload-time = "2026-05-13T12:57:32.267Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/85/545a951eecc270fcd688288c600017e2050a1aacb56c711d208586d3e470/pymdown_extensions-10.21.3-py3-none-any.whl", hash = "sha256:d7a5d08014fc571e80ca21dd6f854e31f94c489800350564d55d15b3c41e76b6", size = 269002, upload-time = "2026-05-13T12:57:30.296Z" }, +] + [[package]] name = "pymssql" version = "2.3.13" @@ -1386,6 +1783,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ca/31/d4e37e9e550c2b92a9cbc2e4d0b7420a27224968580b5a447f420847c975/pytest_xdist-3.8.0-py3-none-any.whl", hash = "sha256:202ca578cfeb7370784a8c33d6d05bc6e13b4f25b5053c30a152269fd10f0b88", size = 46396, upload-time = "2025-07-01T13:30:56.632Z" }, ] +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { 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" }, +] + [[package]] name = "pytokens" version = "0.4.1" @@ -1426,6 +1835,54 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/23/ed/4532e9388e65fa16b46776ef47ad631a64eda1631884488af707666350ed/pywin32-312-cp315-cp315-win_arm64.whl", hash = "sha256:a8597d28f267b39074aef51fa593530082b39cbe5a074226096857b1fed2dfb9", size = 6840337, upload-time = "2026-06-04T07:49:57.531Z" }, ] +[[package]] +name = "pyyaml" +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" }, +] + +[[package]] +name = "pyyaml-env-tag" +version = "1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/2e/79c822141bfd05a853236b504869ebc6b70159afc570e1d5a20641782eaa/pyyaml_env_tag-1.1.tar.gz", hash = "sha256:2eb38b75a2d21ee0475d6d97ec19c63287a7e140231e4214969d0eac923cd7ff", size = 5737, upload-time = "2025-05-13T15:24:01.64Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/11/432f32f8097b03e3cd5fe57e88efb685d964e2e5178a48ed61e841f7fdce/pyyaml_env_tag-1.1-py3-none-any.whl", hash = "sha256:17109e1a528561e32f026364712fee1264bc2ea6715120891174ed1b980d2e04", size = 4722, upload-time = "2025-05-13T15:23:59.629Z" }, +] + [[package]] name = "referencing" version = "0.37.0" @@ -1580,6 +2037,15 @@ 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" }, ] +[[package]] +name = "six" +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" }, +] + [[package]] name = "sqlglot" version = "30.0.3" @@ -1646,6 +2112,18 @@ 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" }, ] +[[package]] +name = "typing-inspection" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { 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" }, +] + [[package]] name = "tzdata" version = "2025.3" @@ -1751,6 +2229,16 @@ dev = [ { name = "ty" }, { name = "vgi-fixtures" }, ] +docs = [ + { name = "mkdocs" }, + { name = "mkdocs-d2-plugin" }, + { name = "mkdocs-material" }, + { name = "mkdocs-section-index" }, + { name = "mkdocstrings", extra = ["python"] }, + { name = "pygments" }, + { name = "pymdown-extensions" }, + { name = "ruff" }, +] [package.metadata] requires-dist = [ @@ -1794,6 +2282,16 @@ dev = [ { name = "ty", specifier = ">=0.0.8" }, { name = "vgi-fixtures", editable = "packages/vgi-fixtures" }, ] +docs = [ + { name = "mkdocs", specifier = ">=1.6" }, + { name = "mkdocs-d2-plugin", specifier = ">=1.4" }, + { name = "mkdocs-material", specifier = ">=9.5" }, + { name = "mkdocs-section-index", specifier = ">=0.3" }, + { name = "mkdocstrings", extras = ["python"], specifier = ">=0.27" }, + { name = "pygments", specifier = ">=2.20" }, + { name = "pymdown-extensions", specifier = ">=10.21.2" }, + { name = "ruff", specifier = ">=0.1" }, +] [[package]] name = "vgi-rpc" @@ -1846,6 +2344,27 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8d/57/a27182528c90ef38d82b636a11f606b0cbb0e17588ed205435f8affe3368/waitress-3.0.2-py3-none-any.whl", hash = "sha256:c56d67fd6e87c2ee598b76abdd4e96cfad1f24cacdea5078d382b1f9d7b5ed2e", size = 56232, upload-time = "2024-11-16T20:02:33.858Z" }, ] +[[package]] +name = "watchdog" +version = "6.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/db/7d/7f3d619e951c88ed75c6037b246ddcf2d322812ee8ea189be89511721d54/watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282", size = 131220, upload-time = "2024-11-01T14:07:13.037Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/68/98/b0345cabdce2041a01293ba483333582891a3bd5769b08eceb0d406056ef/watchdog-6.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:490ab2ef84f11129844c23fb14ecf30ef3d8a6abafd3754a6f75ca1e6654136c", size = 96480, upload-time = "2024-11-01T14:06:42.952Z" }, + { url = "https://files.pythonhosted.org/packages/85/83/cdf13902c626b28eedef7ec4f10745c52aad8a8fe7eb04ed7b1f111ca20e/watchdog-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:76aae96b00ae814b181bb25b1b98076d5fc84e8a53cd8885a318b42b6d3a5134", size = 88451, upload-time = "2024-11-01T14:06:45.084Z" }, + { url = "https://files.pythonhosted.org/packages/fe/c4/225c87bae08c8b9ec99030cd48ae9c4eca050a59bf5c2255853e18c87b50/watchdog-6.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a175f755fc2279e0b7312c0035d52e27211a5bc39719dd529625b1930917345b", size = 89057, upload-time = "2024-11-01T14:06:47.324Z" }, + { url = "https://files.pythonhosted.org/packages/a9/c7/ca4bf3e518cb57a686b2feb4f55a1892fd9a3dd13f470fca14e00f80ea36/watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13", size = 79079, upload-time = "2024-11-01T14:06:59.472Z" }, + { url = "https://files.pythonhosted.org/packages/5c/51/d46dc9332f9a647593c947b4b88e2381c8dfc0942d15b8edc0310fa4abb1/watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379", size = 79078, upload-time = "2024-11-01T14:07:01.431Z" }, + { url = "https://files.pythonhosted.org/packages/d4/57/04edbf5e169cd318d5f07b4766fee38e825d64b6913ca157ca32d1a42267/watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e", size = 79076, upload-time = "2024-11-01T14:07:02.568Z" }, + { url = "https://files.pythonhosted.org/packages/ab/cc/da8422b300e13cb187d2203f20b9253e91058aaf7db65b74142013478e66/watchdog-6.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f", size = 79077, upload-time = "2024-11-01T14:07:03.893Z" }, + { url = "https://files.pythonhosted.org/packages/2c/3b/b8964e04ae1a025c44ba8e4291f86e97fac443bca31de8bd98d3263d2fcf/watchdog-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26", size = 79078, upload-time = "2024-11-01T14:07:05.189Z" }, + { url = "https://files.pythonhosted.org/packages/62/ae/a696eb424bedff7407801c257d4b1afda455fe40821a2be430e173660e81/watchdog-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c", size = 79077, upload-time = "2024-11-01T14:07:06.376Z" }, + { url = "https://files.pythonhosted.org/packages/b5/e8/dbf020b4d98251a9860752a094d09a65e1b436ad181faf929983f697048f/watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2", size = 79078, upload-time = "2024-11-01T14:07:07.547Z" }, + { url = "https://files.pythonhosted.org/packages/07/f6/d0e5b343768e8bcb4cda79f0f2f55051bf26177ecd5651f84c07567461cf/watchdog-6.0.0-py3-none-win32.whl", hash = "sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a", size = 79065, upload-time = "2024-11-01T14:07:09.525Z" }, + { url = "https://files.pythonhosted.org/packages/db/d9/c495884c6e548fce18a8f40568ff120bc3a4b7b99813081c8ac0c936fa64/watchdog-6.0.0-py3-none-win_amd64.whl", hash = "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680", size = 79070, upload-time = "2024-11-01T14:07:10.686Z" }, + { url = "https://files.pythonhosted.org/packages/33/e8/e40370e6d74ddba47f002a32919d91310d6074130fe4e17dabcafc15cbf1/watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f", size = 79067, upload-time = "2024-11-01T14:07:11.845Z" }, +] + [[package]] name = "yarl" version = "1.23.0" diff --git a/vgi/metadata.py b/vgi/metadata.py index 6bc25e2..e4e18a5 100644 --- a/vgi/metadata.py +++ b/vgi/metadata.py @@ -199,11 +199,11 @@ class PartitionKind(Enum): over those columns.""" OVERLAPPING_PARTITIONS = auto() - """Partitions overlap only at boundaries (bounds = [1,2][2,3][3,4]). + """Partitions overlap only at boundaries (bounds = ``[1,2] [2,3] [3,4]``). Wire-level declarable; DuckDB has no consumer today.""" DISJOINT_PARTITIONS = auto() - """Partitions are pairwise disjoint (bounds = [1,2][3,4][5,6]). + """Partitions are pairwise disjoint (bounds = ``[1,2] [3,4] [5,6]``). Wire-level declarable; DuckDB has no consumer today."""