You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@ossie.apache.org by jb...@apache.org on 2026/10/09 04:28:43 UTC
(ossie) branch main updated: Add the wisdom-semantic-view converter (#490)
This is an automated email from the ASF dual-hosted git repository.
jbonofre pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/ossie.git
The following commit(s) were added to refs/heads/main by this push:
new 7b6cefe3 Add the wisdom-semantic-view converter (#490)
7b6cefe3 is described below
commit 7b6cefe38400d7603200da3e881db34606aa185a
Author: Ali Alladin <al...@gmail.com>
AuthorDate: Thu Oct 8 21:28:36 2026 -0700
Add the wisdom-semantic-view converter (#490)
* Add wisdom-semantic-view converter
* Explain how to test the wisdom-semantic-view converter with no accounts
A reviewer with no WisdomAI login and no Snowflake warehouse needs the
commands and the expected diff at the top of the README.
* Address review on enrichment, synonyms, and knowledge ids
---------
Co-authored-by: Ali Alladin <al...@Alis-MacBook-Pro.local>
---
.../converter-wisdom-semantic-view-ci.yml | 68 ++++
converters/README.md | 2 +
converters/wisdom-semantic-view/.gitignore | 25 ++
converters/wisdom-semantic-view/README.md | 142 +++++++
converters/wisdom-semantic-view/pyproject.toml | 65 ++++
.../src/ossie_wisdom_semantic_view/__init__.py | 20 +
.../src/ossie_wisdom_semantic_view/cli.py | 85 ++++
.../src/ossie_wisdom_semantic_view/enrich.py | 110 ++++++
.../src/ossie_wisdom_semantic_view/model.py | 135 +++++++
.../src/ossie_wisdom_semantic_view/pipeline.py | 75 ++++
.../ossie_wisdom_semantic_view/semantic_view.py | 353 +++++++++++++++++
.../src/ossie_wisdom_semantic_view/wisdom.py | 426 +++++++++++++++++++++
.../finance/ossie/cfo_cockpit.enriched.yaml | 113 ++++++
.../tests/fixtures/finance/ossie/cfo_cockpit.yaml | 100 +++++
.../finance/snowflake/cfo_cockpit.after.yaml | 82 ++++
.../finance/snowflake/cfo_cockpit.before.yaml | 73 ++++
.../finance/wisdom/cfo_cockpit.enriched.json | 219 +++++++++++
.../finance/wisdom/cfo_cockpit.imported.json | 202 ++++++++++
.../tests/fixtures/finance/wisdom/enrichment.yaml | 29 ++
.../wisdom-semantic-view/tests/test_packaging.py | 62 +++
.../wisdom-semantic-view/tests/test_round_trip.py | 314 +++++++++++++++
converters/wisdom-semantic-view/uv.lock | 324 ++++++++++++++++
22 files changed, 3024 insertions(+)
diff --git a/.github/workflows/converter-wisdom-semantic-view-ci.yml b/.github/workflows/converter-wisdom-semantic-view-ci.yml
new file mode 100644
index 00000000..d631b187
--- /dev/null
+++ b/.github/workflows/converter-wisdom-semantic-view-ci.yml
@@ -0,0 +1,68 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+
+name: Converters Wisdom Semantic View CI
+on:
+ push:
+ branches: [ "main" ]
+ paths:
+ - 'converters/wisdom-semantic-view/**'
+ - 'core-spec/**'
+ - '.github/workflows/converter-wisdom-semantic-view-ci.yml'
+ pull_request:
+ branches: [ "main" ]
+ paths:
+ - 'converters/wisdom-semantic-view/**'
+ - 'core-spec/**'
+ - '.github/workflows/converter-wisdom-semantic-view-ci.yml'
+
+permissions:
+ contents: read
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ strategy:
+ fail-fast: false
+ matrix:
+ python-version: ["3.11", "3.12", "3.13", "3.14"]
+
+ steps:
+ - name: Checkout project
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
+
+ - name: Set up Python ${{ matrix.python-version }}
+ uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
+ with:
+ python-version: ${{ matrix.python-version }}
+
+ - name: Install uv
+ uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
+ with:
+ version: "0.12.19"
+
+ - name: Sync dependencies
+ working-directory: converters/wisdom-semantic-view
+ run: |
+ uv sync
+
+ - name: Unit Tests
+ working-directory: converters/wisdom-semantic-view
+ run: |
+ uv run pytest
diff --git a/converters/README.md b/converters/README.md
index 62cdf72c..3fbf462b 100644
--- a/converters/README.md
+++ b/converters/README.md
@@ -91,6 +91,8 @@ The Ossie specification currently defines extensions for the following vendors:
| `CUBE` | Cube data model |
| `HEX` | Hex semantic project |
+[`wisdom-semantic-view`](wisdom-semantic-view/README.md) is a separate, fixture-scoped spoke. It reads one Snowflake semantic view, writes one Ossie document, writes a Wisdom domain-export JSON (format `1.0`), and reverses that path. It keeps metric synonyms and `module_custom_instructions.sql_generation`. It does not call `converters/snowflake` (Cortex Analyst export only) or `converters/wisdom` (that spoke drops synonyms and field or metric `ai_context`).
+
Each vendor may define custom extensions (via the `custom_extensions` field in the Ossie spec) to carry vendor-specific metadata that does not have an equivalent in the core specification.
## Mapping Core Constructs
diff --git a/converters/wisdom-semantic-view/.gitignore b/converters/wisdom-semantic-view/.gitignore
new file mode 100644
index 00000000..d6ae8114
--- /dev/null
+++ b/converters/wisdom-semantic-view/.gitignore
@@ -0,0 +1,25 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+__pycache__/
+*.py[cod]
+.pytest_cache/
+*.egg-info/
+dist/
+build/
+.venv/
+venv/
diff --git a/converters/wisdom-semantic-view/README.md b/converters/wisdom-semantic-view/README.md
new file mode 100644
index 00000000..6abb931e
--- /dev/null
+++ b/converters/wisdom-semantic-view/README.md
@@ -0,0 +1,142 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements. See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership. The ASF licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing,
+ software distributed under the License is distributed on an
+ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ KIND, either express or implied. See the License for the
+ specific language governing permissions and limitations
+ under the License.
+-->
+
+## How to test this with no WisdomAI UI, no WisdomAI login, and no Snowflake account or warehouse
+
+You do not need the WisdomAI UI, a WisdomAI login, a Snowflake account, or a Snowflake warehouse. These commands do not sign in to WisdomAI or Snowflake. The inputs are the files in `tests/fixtures/finance/`.
+
+Run this from `converters/wisdom-semantic-view`. You need Python 3.11 or newer and [uv](https://docs.astral.sh/uv/).
+
+```bash
+uv sync
+uv run pytest
+uv run ossie-wisdom-semantic-view to-wisdom \
+ tests/fixtures/finance/snowflake/cfo_cockpit.before.yaml \
+ --enrich tests/fixtures/finance/wisdom/enrichment.yaml \
+ -o /tmp/finance-domain.json
+uv run ossie-wisdom-semantic-view to-semantic-view \
+ /tmp/finance-domain.json \
+ -o /tmp/finance-after.yaml
+# The fixture file starts with the ASF license header. The command writes the YAML only.
+awk 'p{print} /^# under the License\./{p=1}' \
+ tests/fixtures/finance/snowflake/cfo_cockpit.before.yaml \
+ | tail -n +2 > /tmp/finance-before.yaml
+diff -u /tmp/finance-before.yaml /tmp/finance-after.yaml
+```
+
+Passing looks like this. `uv run pytest` exits 0. Both `ossie-wisdom-semantic-view` commands exit 0. `diff -u` prints a unified diff that only adds the `revenue` metric, the synonyms `revenue`, `net revenue`, and `board revenue`, and the February fiscal-year `sql_generation` line. `diff` exits 1 because those lines are new. That is the result you want. The February line is demo copy.
+
+# Apache Ossie Wisdom semantic-view converter
+
+Offline conversion of one Snowflake semantic view through an Apache Ossie document
+into a WisdomAI domain export (format `1.0`), and back. No Snowflake account and no
+Wisdom tenant.
+
+This is not [`converters/snowflake`](../snowflake). That package only writes Cortex
+Analyst YAML and cannot read a semantic view. This is not [`converters/wisdom`](../wisdom).
+That package converts domain-export JSON both ways and drops synonyms and field or
+metric `ai_context`. Calling either one drops the revenue metric's synonyms and the
+`sql_generation` instruction this fixture carries.
+
+Scope is the finance model under `tests/fixtures/finance/` and the keys that model
+uses. A key outside that list, or an expression other than a bare column name or
+`SUM(bookings.amount)`, exits `1` and prints the key or expression. Identifier case
+is preserved.
+
+## Installation
+
+```bash
+# from a checkout of this directory:
+pip install -e .
+```
+
+The only runtime dependency is `PyYAML`. Python 3.11+.
+
+## Usage
+
+```bash
+ossie-wisdom-semantic-view to-wisdom <view.yaml> -o <domain.json> [--enrich <enrichment.yaml>]
+ossie-wisdom-semantic-view to-semantic-view <domain.json> -o <view.yaml>
+```
+
+`to-wisdom` reads a semantic view, builds one Ossie document (`version` `0.2.0.dev0`),
+and writes the domain export. `--enrich` is the only step that adds a name the view
+did not already have. On this fixture it adds the `revenue` metric, three synonyms,
+and the February fiscal-year instruction.
+
+`to-semantic-view` reads the domain export, builds the Ossie document, and writes
+the semantic view. Synonyms come back as the metric `synonyms` array. The knowledge
+item named `Board revenue` comes back as `module_custom_instructions.sql_generation`.
+`synonym_sets` stays empty; a non-empty `items_json` exits `1`.
+
+## Mapping
+
+Semantic view → Ossie:
+
+| Semantic view | Ossie |
+|---|---|
+| `name`, `description` | `name`, `description` |
+| `tables[]` | `datasets[]` |
+| `base_table` database, schema, table | `source` as `database.schema.table` |
+| `primary_key.columns` | `primary_key` |
+| `dimensions[]` | fields, `dimension.is_time: false` |
+| `time_dimensions[]` | fields, `dimension.is_time: true` |
+| `facts[]` | fields, no `dimension` block |
+| `name`, `expr`, `data_type` | field `name`, dialect `SNOWFLAKE` expression copied verbatim, datatype from the table below |
+| `relationships[].left_table` / `right_table` | `from` / `to` |
+| `relationship_columns[]` | `from_columns` / `to_columns` |
+| `metrics[]` `name`, `description`, `expr` | metrics, expression dialect `SNOWFLAKE` |
+| `metrics[].synonyms` | metric `ai_context.synonyms` |
+| `module_custom_instructions.sql_generation` | model `ai_context.instructions` |
+
+This fixture's relationship is many-to-one with `bookings` on the left. The mapper
+checks that. It does not infer cardinality for any other relationship.
+
+Datatypes, this fixture only: `VARCHAR` → `String`, `NUMBER(38,0)` → `Integer`,
+`NUMBER(18,2)` → `Decimal`, `DATE` → `Date`.
+
+Ossie → Wisdom format `1.0`:
+
+- `version` is `"1.0"`.
+- `export_metadata.exported_at` is `"2026-01-01T00:00:00+00:00"`. `source_domain_id`
+ is `"demo-finance-cfo"`. `domain_name` is the model name. These are constants.
+- Domain `ref.name` and `description` come from the model. `domainSystemInstructions`
+ is the model description.
+- Each dataset becomes `tables[].zsheet_json`. `location` splits `source` into
+ database, schema, `dbTable`, and `connectionId` `et-connection-snowflake`. A field
+ whose expression is its own name becomes a column. Metrics become `measures` on
+ `bookings`.
+- One relationship, `relationshipType` `MANY_TO_ONE`, a single `joinCondition`.
+- `connections` is one object, dialect `snowflake`, id `et-connection-snowflake`.
+- `reviewed_queries` and `synonym_sets` are `{"ref": null, "items_json": "{}"}`.
+- Ids are functions of names (`ET_DOMAIN_finance_cfo_cockpit`, `ET_ZSHEET_bookings`,
+ `ET_ZSHEET_fiscal_calendar`) so a second run is byte-identical.
+- Metric `ai_context.synonyms` become a knowledge item named `Synonyms for <metric>`
+ whose content is markdown bullets. Model `ai_context.instructions` become a
+ knowledge item named `Board revenue`.
+
+The reverse functions invert the same rows. `verified_queries`, filters, tags, and
+`access_modifier` are not written.
+
+## Development
+
+```bash
+uv sync
+uv run pytest
+```
diff --git a/converters/wisdom-semantic-view/pyproject.toml b/converters/wisdom-semantic-view/pyproject.toml
new file mode 100644
index 00000000..058bad7b
--- /dev/null
+++ b/converters/wisdom-semantic-view/pyproject.toml
@@ -0,0 +1,65 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+[build-system]
+requires = ["hatchling"]
+build-backend = "hatchling.build"
+
+[dependency-groups]
+dev = [
+ "pytest>=8.0",
+ # Dev/test-only: validates the finance Ossie documents against
+ # core-spec/ossie-schema.json. The only runtime dependency stays PyYAML.
+ "jsonschema>=4.18",
+]
+
+[project]
+name = "apache-ossie-wisdom-semantic-view"
+version = "0.1.0"
+description = "One Snowflake semantic view through Apache Ossie into a WisdomAI domain export, and back"
+authors = [{ name = "Apache Software Foundation", email = "dev@ossie.apache.org" }]
+requires-python = ">=3.11"
+readme = "README.md"
+license = "Apache-2.0"
+keywords = [
+ "Apache Ossie",
+ "Ossie",
+ "WisdomAI",
+ "Snowflake"
+]
+dependencies = [
+ "PyYAML>=6.0",
+]
+
+[project.scripts]
+ossie-wisdom-semantic-view = "ossie_wisdom_semantic_view.cli:main"
+
+[project.urls]
+homepage = "https://ossie.apache.org/"
+repository = "https://github.com/apache/ossie/"
+
+[tool.hatch.build.targets.wheel]
+packages = ["src/ossie_wisdom_semantic_view"]
+
+[tool.pytest.ini_options]
+testpaths = ["tests"]
+
+[tool.uv]
+required-version = ">=0.9.0"
+default-groups = [
+ "dev"
+]
diff --git a/converters/wisdom-semantic-view/src/ossie_wisdom_semantic_view/__init__.py b/converters/wisdom-semantic-view/src/ossie_wisdom_semantic_view/__init__.py
new file mode 100644
index 00000000..5146d548
--- /dev/null
+++ b/converters/wisdom-semantic-view/src/ossie_wisdom_semantic_view/__init__.py
@@ -0,0 +1,20 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+"""Snowflake semantic view ↔ Ossie ↔ Wisdom domain export, for one finance model."""
+
+__version__ = "0.1.0"
diff --git a/converters/wisdom-semantic-view/src/ossie_wisdom_semantic_view/cli.py b/converters/wisdom-semantic-view/src/ossie_wisdom_semantic_view/cli.py
new file mode 100644
index 00000000..19c1567f
--- /dev/null
+++ b/converters/wisdom-semantic-view/src/ossie_wisdom_semantic_view/cli.py
@@ -0,0 +1,85 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+"""Command-line interface for the finance semantic-view converter.
+
+ ossie-wisdom-semantic-view to-wisdom <view.yaml> -o <domain.json> [--enrich <file>]
+ ossie-wisdom-semantic-view to-semantic-view <domain.json> -o <view.yaml>
+
+`to-wisdom` reads one Snowflake semantic view and writes a Wisdom domain-export
+JSON (format 1.0). The Ossie document is the step in between and is not written.
+`--enrich` applies one enrichment file after that import.
+
+`to-semantic-view` reads that JSON and writes a Snowflake semantic view. Metric
+synonyms and the business-context instruction survive that direction.
+
+Unknown keys and unsupported expressions exit 1. This command does not call
+ossie-snowflake or ossie-wisdom.
+"""
+
+from __future__ import annotations
+
+import argparse
+import sys
+from pathlib import Path
+
+from ossie_wisdom_semantic_view.pipeline import semantic_view_to_wisdom, wisdom_to_semantic_view
+
+
+def _build_parser() -> argparse.ArgumentParser:
+ parser = argparse.ArgumentParser(
+ prog="ossie-wisdom-semantic-view",
+ description=__doc__,
+ formatter_class=argparse.RawDescriptionHelpFormatter,
+ )
+ sub = parser.add_subparsers(dest="command")
+ sub.required = True
+
+ to_wisdom = sub.add_parser(
+ "to-wisdom",
+ help="Snowflake semantic view → Ossie → Wisdom domain-export JSON",
+ )
+ to_wisdom.add_argument("view", metavar="VIEW", help="Snowflake semantic view YAML")
+ to_wisdom.add_argument("-o", "--output", required=True, metavar="FILE", help="Wisdom domain-export JSON")
+ to_wisdom.add_argument(
+ "--enrich",
+ metavar="FILE",
+ help="enrichment YAML applied to the domain export after import",
+ )
+
+ to_view = sub.add_parser(
+ "to-semantic-view",
+ help="Wisdom domain-export JSON → Ossie → Snowflake semantic view",
+ )
+ to_view.add_argument("domain", metavar="DOMAIN", help="Wisdom domain-export JSON")
+ to_view.add_argument("-o", "--output", required=True, metavar="FILE", help="Snowflake semantic view YAML")
+ return parser
+
+
+def main(argv: list[str] | None = None) -> int:
+ args = _build_parser().parse_args(argv)
+ if args.command == "to-wisdom":
+ enrichment = Path(args.enrich).read_text() if args.enrich else None
+ text = semantic_view_to_wisdom(Path(args.view).read_text(), enrichment)
+ else:
+ text = wisdom_to_semantic_view(Path(args.domain).read_text())
+ Path(args.output).write_text(text)
+ return 0
+
+
+if __name__ == "__main__":
+ sys.exit(main())
diff --git a/converters/wisdom-semantic-view/src/ossie_wisdom_semantic_view/enrich.py b/converters/wisdom-semantic-view/src/ossie_wisdom_semantic_view/enrich.py
new file mode 100644
index 00000000..1d226416
--- /dev/null
+++ b/converters/wisdom-semantic-view/src/ossie_wisdom_semantic_view/enrich.py
@@ -0,0 +1,110 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+"""The only place a name that was not in the semantic view is added."""
+
+from __future__ import annotations
+
+import copy
+
+from ossie_wisdom_semantic_view.model import check_metric_expression, die, require_keys
+from ossie_wisdom_semantic_view.semantic_view import MANY_SIDE
+from ossie_wisdom_semantic_view.wisdom import (
+ knowledge_id,
+ measure_id,
+ reject_duplicate_knowledge_ids,
+ reject_nonempty_synonym_sets,
+)
+
+_ENRICHMENT_KEYS = {"metric", "knowledge"}
+_METRIC_KEYS = {"name", "expr", "description"}
+_KNOWLEDGE_KEYS = {"name", "content"}
+
+
+def load_enrichment(text: str) -> dict:
+ import yaml
+
+ loaded = yaml.safe_load(text)
+ require_keys(loaded, _ENRICHMENT_KEYS)
+ for key in ("metric", "knowledge"):
+ if key not in loaded:
+ die(key)
+ return loaded
+
+
+def apply_enrichment(export: dict, enrichment: dict) -> dict:
+ require_keys(enrichment, _ENRICHMENT_KEYS)
+ metric = require_keys(enrichment["metric"], _METRIC_KEYS)
+ for key in ("name", "expr", "description"):
+ if key not in metric:
+ die(key)
+ dataset_names = [table["zsheet_json"]["ref"]["name"] for table in export["tables"]]
+ expression = metric["expr"]
+ if not isinstance(expression, str):
+ die("expr")
+ check_metric_expression(expression, dataset_names)
+
+ enriched = copy.deepcopy(export)
+ reject_nonempty_synonym_sets(enriched)
+ bookings = None
+ for table in enriched["tables"]:
+ zsheet = table["zsheet_json"]
+ if zsheet["ref"]["name"] == MANY_SIDE:
+ bookings = zsheet
+ if bookings is None:
+ die(MANY_SIDE)
+ measures = bookings.setdefault("measures", [])
+ if any(item["name"] == metric["name"] for item in measures):
+ die(metric["name"])
+ measures.append(
+ {
+ "name": metric["name"],
+ "expression": expression,
+ "description": metric["description"],
+ "id": measure_id(metric["name"]),
+ }
+ )
+
+ knowledge = enriched["domain"]["zsheet_json"].setdefault("knowledge", [])
+ blocks = enrichment["knowledge"]
+ if not isinstance(blocks, list) or not blocks:
+ die("knowledge")
+ existing_names = {item.get("name") for item in knowledge}
+ prepared: list[dict] = []
+ for block in blocks:
+ require_keys(block, _KNOWLEDGE_KEYS)
+ for key in ("name", "content"):
+ if key not in block:
+ die(key)
+ name = block["name"]
+ if name in existing_names:
+ die(f"{name} already exists")
+ existing_names.add(name)
+ content = block["content"]
+ if not isinstance(content, str):
+ die("content")
+ prepared.append(
+ {
+ "name": name,
+ "content": content.rstrip("\n"),
+ "id": knowledge_id(name),
+ }
+ )
+ knowledge.extend(prepared)
+ reject_duplicate_knowledge_ids(knowledge)
+ reject_nonempty_synonym_sets(enriched)
+ return enriched
diff --git a/converters/wisdom-semantic-view/src/ossie_wisdom_semantic_view/model.py b/converters/wisdom-semantic-view/src/ossie_wisdom_semantic_view/model.py
new file mode 100644
index 00000000..ea2ae96e
--- /dev/null
+++ b/converters/wisdom-semantic-view/src/ossie_wisdom_semantic_view/model.py
@@ -0,0 +1,135 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+"""Shared checks for the one finance Ossie document."""
+
+from __future__ import annotations
+
+import re
+import sys
+
+import yaml
+
+SPEC_VERSION = "0.2.0.dev0"
+BARE_COLUMN = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*$")
+ALLOWED_AGGREGATE = "SUM(bookings.amount)"
+
+# Snowflake semantic-view data_type to Ossie datatype. This fixture only.
+SNOWFLAKE_TO_OSSIE = {
+ "VARCHAR": "String",
+ "NUMBER(38,0)": "Integer",
+ "NUMBER(18,2)": "Decimal",
+ "DATE": "Date",
+}
+OSSIE_TO_SNOWFLAKE = {ossie: snowflake for snowflake, ossie in SNOWFLAKE_TO_OSSIE.items()}
+
+# Ossie datatype to the Wisdom column dataType used in the format 1.0 subset.
+OSSIE_TO_WISDOM = {
+ "String": "VARCHAR",
+ "Integer": "INT64",
+ "Decimal": "DECIMAL",
+ "Date": "DATE",
+}
+WISDOM_TO_OSSIE = {wisdom: ossie for ossie, wisdom in OSSIE_TO_WISDOM.items()}
+
+
+def die(message: str) -> None:
+ print(message, file=sys.stderr)
+ raise SystemExit(1)
+
+
+def reject_key(key: str) -> None:
+ print(key, file=sys.stderr)
+ raise SystemExit(1)
+
+
+def require_keys(node: object, allowed: set[str]) -> dict:
+ if not isinstance(node, dict):
+ die("mapping")
+ for key in node:
+ if key not in allowed:
+ reject_key(str(key))
+ return node
+
+
+def check_expression(expression: str) -> None:
+ if expression == ALLOWED_AGGREGATE or BARE_COLUMN.fullmatch(expression):
+ return
+ die(f"unsupported expression: {expression}")
+
+
+def referenced_datasets(expression: str, dataset_names: list[str]) -> list[str]:
+ found = []
+ for name in dataset_names:
+ if re.search(rf"(?<![A-Za-z0-9_]){re.escape(name)}\.", expression):
+ found.append(name)
+ return found
+
+
+def check_metric_expression(expression: str, dataset_names: list[str]) -> None:
+ check_expression(expression)
+ named = referenced_datasets(expression, dataset_names)
+ if named != ["bookings"]:
+ die(expression)
+
+
+def snowflake_expression(expression: str) -> dict:
+ check_expression(expression)
+ return {"dialects": [{"dialect": "SNOWFLAKE", "expression": expression}]}
+
+
+def only_snowflake(expression: dict) -> str:
+ dialects = expression.get("dialects") if isinstance(expression, dict) else None
+ if not isinstance(dialects, list) or len(dialects) != 1:
+ die("dialects")
+ entry = dialects[0]
+ if not isinstance(entry, dict) or entry.get("dialect") != "SNOWFLAKE":
+ reject_key(str(entry.get("dialect", "dialect")) if isinstance(entry, dict) else "dialect")
+ value = entry.get("expression")
+ if not isinstance(value, str):
+ die("expression")
+ check_expression(value)
+ return value
+
+
+def dimension_for_datatype(datatype: str) -> dict | None:
+ """Role for this fixture's four datatypes.
+
+ Decimals are facts. Dates are time dimensions. Strings and integers are
+ dimensions. Wisdom columns do not carry a dimension block, so the reverse
+ mapping restores the block from the datatype.
+ """
+
+ if datatype == "Decimal":
+ return None
+ if datatype == "Date":
+ return {"is_time": True}
+ if datatype in ("String", "Integer"):
+ return {"is_time": False}
+ die(datatype)
+ return None
+
+
+def dump_ossie(model: dict) -> str:
+ return yaml.safe_dump(model, sort_keys=False, width=4096, allow_unicode=True)
+
+
+def load_ossie(text: str) -> dict:
+ loaded = yaml.safe_load(text)
+ if not isinstance(loaded, dict):
+ die("version")
+ return loaded
diff --git a/converters/wisdom-semantic-view/src/ossie_wisdom_semantic_view/pipeline.py b/converters/wisdom-semantic-view/src/ossie_wisdom_semantic_view/pipeline.py
new file mode 100644
index 00000000..2ae9ae03
--- /dev/null
+++ b/converters/wisdom-semantic-view/src/ossie_wisdom_semantic_view/pipeline.py
@@ -0,0 +1,75 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+"""Semantic view → Ossie → Wisdom, and the reverse, for this finance model."""
+
+from __future__ import annotations
+
+from dataclasses import dataclass
+
+from ossie_wisdom_semantic_view.enrich import apply_enrichment, load_enrichment
+from ossie_wisdom_semantic_view.model import dump_ossie
+from ossie_wisdom_semantic_view.semantic_view import (
+ dump_semantic_view,
+ load_semantic_view,
+ ossie_to_semantic_view,
+ semantic_view_to_ossie,
+)
+from ossie_wisdom_semantic_view.wisdom import dump_wisdom, load_wisdom, ossie_to_wisdom, wisdom_to_ossie
+
+
+@dataclass(frozen=True)
+class Artifacts:
+ ossie_yaml: str
+ wisdom_imported_json: str
+ wisdom_enriched_json: str
+ ossie_enriched_yaml: str
+ semantic_after_yaml: str
+
+
+def semantic_view_to_wisdom(view_yaml: str, enrichment_yaml: str | None = None) -> str:
+ """Semantic view YAML → one Ossie document → Wisdom domain-export JSON."""
+
+ model = semantic_view_to_ossie(load_semantic_view(view_yaml))
+ export = ossie_to_wisdom(model)
+ if enrichment_yaml is not None:
+ export = apply_enrichment(export, load_enrichment(enrichment_yaml))
+ return dump_wisdom(export)
+
+
+def wisdom_to_semantic_view(domain_json: str) -> str:
+ """Wisdom domain-export JSON → Ossie → Snowflake semantic view YAML."""
+
+ model = wisdom_to_ossie(load_wisdom(domain_json))
+ return dump_semantic_view(ossie_to_semantic_view(model))
+
+
+def convert_finance(before_yaml: str, enrichment_yaml: str) -> Artifacts:
+ """The finance fixture, both directions, including the one enrichment file."""
+
+ ossie = semantic_view_to_ossie(load_semantic_view(before_yaml))
+ imported = ossie_to_wisdom(ossie)
+ enriched = apply_enrichment(imported, load_enrichment(enrichment_yaml))
+ enriched_ossie = wisdom_to_ossie(enriched)
+ after = ossie_to_semantic_view(enriched_ossie)
+ return Artifacts(
+ ossie_yaml=dump_ossie(ossie),
+ wisdom_imported_json=dump_wisdom(imported),
+ wisdom_enriched_json=dump_wisdom(enriched),
+ ossie_enriched_yaml=dump_ossie(enriched_ossie),
+ semantic_after_yaml=dump_semantic_view(after),
+ )
diff --git a/converters/wisdom-semantic-view/src/ossie_wisdom_semantic_view/semantic_view.py b/converters/wisdom-semantic-view/src/ossie_wisdom_semantic_view/semantic_view.py
new file mode 100644
index 00000000..a14567cc
--- /dev/null
+++ b/converters/wisdom-semantic-view/src/ossie_wisdom_semantic_view/semantic_view.py
@@ -0,0 +1,353 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+"""Snowflake semantic-view YAML, both directions, for this finance model only."""
+
+from __future__ import annotations
+
+import yaml
+
+from ossie_wisdom_semantic_view.model import (
+ SPEC_VERSION,
+ check_expression,
+ check_metric_expression,
+ die,
+ dimension_for_datatype,
+ only_snowflake,
+ require_keys,
+ snowflake_expression,
+ SNOWFLAKE_TO_OSSIE,
+ OSSIE_TO_SNOWFLAKE,
+)
+
+RELATIONSHIP_NAME = "bookings_to_fiscal_calendar"
+MANY_SIDE = "bookings"
+ONE_SIDE = "fiscal_calendar"
+
+_VIEW_KEYS = {
+ "name",
+ "description",
+ "tables",
+ "relationships",
+ "metrics",
+ "module_custom_instructions",
+}
+_TABLE_KEYS = {
+ "name",
+ "base_table",
+ "primary_key",
+ "dimensions",
+ "time_dimensions",
+ "facts",
+}
+_BASE_TABLE_KEYS = {"database", "schema", "table"}
+_PRIMARY_KEY_KEYS = {"columns"}
+_COLUMN_KEYS = {"name", "expr", "data_type"}
+_RELATIONSHIP_KEYS = {"name", "left_table", "right_table", "relationship_columns"}
+_RELATIONSHIP_COLUMN_KEYS = {"left_column", "right_column"}
+_METRIC_KEYS = {"name", "description", "expr", "synonyms"}
+_MODULE_KEYS = {"sql_generation"}
+
+
+def load_semantic_view(text: str) -> dict:
+ loaded = yaml.safe_load(text)
+ if not isinstance(loaded, dict):
+ die("name")
+ return loaded
+
+
+def dump_semantic_view(view: dict) -> str:
+ return yaml.safe_dump(view, sort_keys=False, width=4096, allow_unicode=True)
+
+
+def semantic_view_to_ossie(view: dict) -> dict:
+ require_keys(view, _VIEW_KEYS)
+ for key in ("name", "description", "tables", "relationships", "metrics"):
+ if key not in view:
+ die(key)
+ datasets = [_table_to_dataset(table) for table in _as_list(view["tables"], "tables")]
+ relationships = [
+ _relationship_to_ossie(item) for item in _as_list(view["relationships"], "relationships")
+ ]
+ dataset_names = [dataset["name"] for dataset in datasets]
+ metrics = [
+ _metric_to_ossie(item, dataset_names) for item in _as_list(view["metrics"], "metrics")
+ ]
+ model = {
+ "version": SPEC_VERSION,
+ "name": view["name"],
+ "description": view["description"],
+ }
+ instructions = view.get("module_custom_instructions")
+ if instructions is not None:
+ require_keys(instructions, _MODULE_KEYS)
+ if "sql_generation" not in instructions:
+ die("sql_generation")
+ model["ai_context"] = {"instructions": instructions["sql_generation"]}
+ model["datasets"] = datasets
+ model["relationships"] = relationships
+ model["metrics"] = metrics
+ return model
+
+
+def ossie_to_semantic_view(model: dict) -> dict:
+ if model.get("version") != SPEC_VERSION:
+ die(str(model.get("version")))
+ tables = [_dataset_to_table(dataset) for dataset in model["datasets"]]
+ relationships = [_ossie_relationship_to_view(item) for item in model.get("relationships") or []]
+ dataset_names = [dataset["name"] for dataset in model["datasets"]]
+ metrics = [
+ _ossie_metric_to_view(item, dataset_names) for item in model.get("metrics") or []
+ ]
+ view = {
+ "name": model["name"],
+ "description": model["description"],
+ "tables": tables,
+ "relationships": relationships,
+ "metrics": metrics,
+ }
+ ai_context = model.get("ai_context")
+ if ai_context is not None:
+ require_keys(ai_context, {"instructions"})
+ if "instructions" not in ai_context:
+ die("instructions")
+ view["module_custom_instructions"] = {"sql_generation": ai_context["instructions"]}
+ return view
+
+
+def _table_to_dataset(table: dict) -> dict:
+ require_keys(table, _TABLE_KEYS)
+ for key in ("name", "base_table", "primary_key"):
+ if key not in table:
+ die(key)
+ base_table = require_keys(table["base_table"], _BASE_TABLE_KEYS)
+ for key in ("database", "schema", "table"):
+ if key not in base_table:
+ die(key)
+ primary_key = require_keys(table["primary_key"], _PRIMARY_KEY_KEYS)
+ columns = primary_key.get("columns")
+ if not isinstance(columns, list) or not columns:
+ die("columns")
+ fields = []
+ for column in _as_list(table.get("dimensions", []), "dimensions"):
+ fields.append(_attribute_to_field(column, is_time=False))
+ for column in _as_list(table.get("time_dimensions", []), "time_dimensions"):
+ fields.append(_attribute_to_field(column, is_time=True))
+ for column in _as_list(table.get("facts", []), "facts"):
+ fields.append(_fact_to_field(column))
+ return {
+ "name": table["name"],
+ "source": f"{base_table['database']}.{base_table['schema']}.{base_table['table']}",
+ "primary_key": list(columns),
+ "fields": fields,
+ }
+
+
+def _attribute_to_field(column: dict, *, is_time: bool) -> dict:
+ name, expression, datatype = _column_parts(column)
+ if expression != name:
+ die(expression)
+ expected = dimension_for_datatype(datatype)
+ if expected != {"is_time": is_time}:
+ die(column["data_type"])
+ return {
+ "name": name,
+ "expression": snowflake_expression(expression),
+ "datatype": datatype,
+ "dimension": {"is_time": is_time},
+ }
+
+
+def _fact_to_field(column: dict) -> dict:
+ name, expression, datatype = _column_parts(column)
+ if expression != name:
+ die(expression)
+ if dimension_for_datatype(datatype) is not None:
+ die(column["data_type"])
+ return {
+ "name": name,
+ "expression": snowflake_expression(expression),
+ "datatype": datatype,
+ }
+
+
+def _column_parts(column: dict) -> tuple[str, str, str]:
+ require_keys(column, _COLUMN_KEYS)
+ for key in ("name", "expr", "data_type"):
+ if key not in column:
+ die(key)
+ expression = column["expr"]
+ if not isinstance(expression, str):
+ die("expr")
+ check_expression(expression)
+ data_type = column["data_type"]
+ if data_type not in SNOWFLAKE_TO_OSSIE:
+ die(str(data_type))
+ return column["name"], expression, SNOWFLAKE_TO_OSSIE[data_type]
+
+
+def _relationship_to_ossie(relationship: dict) -> dict:
+ require_keys(relationship, _RELATIONSHIP_KEYS)
+ for key in ("name", "left_table", "right_table", "relationship_columns"):
+ if key not in relationship:
+ die(key)
+ # This file is many-to-one with bookings on the left. Do not infer
+ # cardinality for any other relationship.
+ if (
+ relationship["name"] != RELATIONSHIP_NAME
+ or relationship["left_table"] != MANY_SIDE
+ or relationship["right_table"] != ONE_SIDE
+ ):
+ die(str(relationship["name"]))
+ pairs = _as_list(relationship["relationship_columns"], "relationship_columns")
+ if len(pairs) != 1:
+ die("relationship_columns")
+ pair = require_keys(pairs[0], _RELATIONSHIP_COLUMN_KEYS)
+ for key in ("left_column", "right_column"):
+ if key not in pair:
+ die(key)
+ return {
+ "name": relationship["name"],
+ "from": relationship["left_table"],
+ "to": relationship["right_table"],
+ "from_columns": [pair["left_column"]],
+ "to_columns": [pair["right_column"]],
+ }
+
+
+def _metric_to_ossie(metric: dict, dataset_names: list[str]) -> dict:
+ require_keys(metric, _METRIC_KEYS)
+ for key in ("name", "description", "expr"):
+ if key not in metric:
+ die(key)
+ expression = metric["expr"]
+ if not isinstance(expression, str):
+ die("expr")
+ check_metric_expression(expression, dataset_names)
+ converted = {
+ "name": metric["name"],
+ "description": metric["description"],
+ "expression": snowflake_expression(expression),
+ }
+ if "synonyms" in metric:
+ synonyms = _import_synonyms(metric["synonyms"])
+ if synonyms:
+ converted["ai_context"] = {"synonyms": synonyms}
+ return converted
+
+
+def _import_synonyms(synonyms: object) -> list[str]:
+ if not isinstance(synonyms, list) or not all(isinstance(item, str) for item in synonyms):
+ die("synonyms")
+ # An empty list is the same as an omitted key. _knowledge_from_model
+ # drops empty lists, so keeping [] would not round-trip.
+ if not synonyms:
+ return []
+ seen: set[str] = set()
+ for synonym in synonyms:
+ if "\n" in synonym or "\r" in synonym or synonym in seen:
+ die("synonyms")
+ seen.add(synonym)
+ return list(synonyms)
+
+
+def _dataset_to_table(dataset: dict) -> dict:
+ parts = str(dataset["source"]).split(".")
+ if len(parts) != 3 or not all(parts):
+ die(str(dataset["source"]))
+ database, schema, table_name = parts
+ dimensions: list[dict] = []
+ time_dimensions: list[dict] = []
+ facts: list[dict] = []
+ for field in dataset["fields"]:
+ expression = only_snowflake(field["expression"])
+ if expression != field["name"]:
+ die(expression)
+ datatype = field["datatype"]
+ if datatype not in OSSIE_TO_SNOWFLAKE:
+ die(str(datatype))
+ column = {
+ "name": field["name"],
+ "expr": expression,
+ "data_type": OSSIE_TO_SNOWFLAKE[datatype],
+ }
+ if "dimension" in field:
+ is_time = field["dimension"].get("is_time")
+ expected = dimension_for_datatype(datatype)
+ if expected != {"is_time": is_time}:
+ die(field["name"])
+ if is_time:
+ time_dimensions.append(column)
+ else:
+ dimensions.append(column)
+ else:
+ if dimension_for_datatype(datatype) is not None:
+ die(field["name"])
+ facts.append(column)
+ table = {
+ "name": dataset["name"],
+ "base_table": {"database": database, "schema": schema, "table": table_name},
+ "primary_key": {"columns": list(dataset["primary_key"])},
+ }
+ if dimensions:
+ table["dimensions"] = dimensions
+ if time_dimensions:
+ table["time_dimensions"] = time_dimensions
+ if facts:
+ table["facts"] = facts
+ return table
+
+
+def _ossie_relationship_to_view(relationship: dict) -> dict:
+ if relationship.get("from") != MANY_SIDE or relationship.get("to") != ONE_SIDE:
+ die(str(relationship.get("name")))
+ from_columns = relationship.get("from_columns") or []
+ to_columns = relationship.get("to_columns") or []
+ if len(from_columns) != 1 or len(to_columns) != 1:
+ die("relationship_columns")
+ return {
+ "name": relationship["name"],
+ "left_table": relationship["from"],
+ "right_table": relationship["to"],
+ "relationship_columns": [
+ {"left_column": from_columns[0], "right_column": to_columns[0]}
+ ],
+ }
+
+
+def _ossie_metric_to_view(metric: dict, dataset_names: list[str]) -> dict:
+ expression = only_snowflake(metric["expression"])
+ check_metric_expression(expression, dataset_names)
+ converted = {
+ "name": metric["name"],
+ "description": metric["description"],
+ "expr": expression,
+ }
+ ai_context = metric.get("ai_context")
+ if ai_context is not None:
+ require_keys(ai_context, {"synonyms"})
+ synonyms = ai_context.get("synonyms")
+ if not isinstance(synonyms, list):
+ die("synonyms")
+ converted["synonyms"] = list(synonyms)
+ return converted
+
+
+def _as_list(value: object, key: str) -> list:
+ if not isinstance(value, list):
+ die(key)
+ return value
diff --git a/converters/wisdom-semantic-view/src/ossie_wisdom_semantic_view/wisdom.py b/converters/wisdom-semantic-view/src/ossie_wisdom_semantic_view/wisdom.py
new file mode 100644
index 00000000..54af18be
--- /dev/null
+++ b/converters/wisdom-semantic-view/src/ossie_wisdom_semantic_view/wisdom.py
@@ -0,0 +1,426 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+"""Wisdom domain-export JSON, format 1.0, both directions, for this finance model."""
+
+from __future__ import annotations
+
+import json
+import re
+
+from ossie_wisdom_semantic_view.model import (
+ SPEC_VERSION,
+ WISDOM_TO_OSSIE,
+ check_expression,
+ check_metric_expression,
+ die,
+ dimension_for_datatype,
+ only_snowflake,
+ OSSIE_TO_WISDOM,
+ snowflake_expression,
+)
+from ossie_wisdom_semantic_view.semantic_view import MANY_SIDE, ONE_SIDE, RELATIONSHIP_NAME
+
+# Constants for this offline demo. They are not read from a Wisdom tenant.
+# domain_name is the model name. For this fixture that name is finance_cfo_cockpit.
+EXPORTED_AT = "2026-01-01T00:00:00+00:00"
+SOURCE_DOMAIN_ID = "demo-finance-cfo"
+CONNECTION_ID = "et-connection-snowflake"
+BOARD_REVENUE = "Board revenue"
+SYNONYM_PREFIX = "Synonyms for "
+
+
+def dump_wisdom(export: dict) -> str:
+ return json.dumps(export, indent=2) + "\n"
+
+
+def load_wisdom(text: str) -> dict:
+ loaded = json.loads(text)
+ if not isinstance(loaded, dict):
+ die("version")
+ return loaded
+
+
+def domain_uuid(name: str) -> str:
+ return f"ET_DOMAIN_{name}"
+
+
+def zsheet_uuid(name: str) -> str:
+ return f"ET_ZSHEET_{name}"
+
+
+def knowledge_id(name: str) -> str:
+ slug = re.sub(r"[^a-z0-9]+", "_", name.lower()).strip("_")
+ return f"ET_UNSTRUCTURED_KNOWLEDGE_{slug}"
+
+
+def measure_id(name: str) -> str:
+ return f"MEASURE_{name}"
+
+
+def empty_item_list() -> dict:
+ return {"ref": None, "items_json": "{}"}
+
+
+def reject_duplicate_knowledge_ids(items: list[dict]) -> None:
+ """Fail when two knowledge names slugify to the same id. Do not suffix."""
+
+ seen: dict[str, str] = {}
+ for item in items:
+ item_id = item["id"]
+ name = item["name"]
+ if item_id in seen:
+ die(f"duplicate knowledge id: {item_id} ({seen[item_id]}, {name})")
+ seen[item_id] = name
+
+
+def reject_nonempty_synonym_sets(export: dict) -> None:
+ synonym_sets = export.get("synonym_sets", {})
+ # A present null is an empty set, not a missing dict.
+ if synonym_sets is None:
+ return
+ if not isinstance(synonym_sets, dict):
+ die("synonym_sets")
+ raw = synonym_sets.get("items_json", "{}")
+ if raw is None:
+ return
+ if not isinstance(raw, str):
+ die("synonym_sets.items_json")
+ try:
+ parsed = json.loads(raw)
+ except json.JSONDecodeError:
+ die("synonym_sets.items_json")
+ if parsed is None:
+ return
+ if not isinstance(parsed, dict) or parsed != {}:
+ die("synonym_sets.items_json")
+
+
+def ossie_to_wisdom(model: dict) -> dict:
+ if model.get("version") != SPEC_VERSION:
+ die(str(model.get("version")))
+ name = model["name"]
+ dataset_names = [dataset["name"] for dataset in model["datasets"]]
+ measures_by_dataset: dict[str, list[dict]] = {dataset_name: [] for dataset_name in dataset_names}
+ for metric in model.get("metrics") or []:
+ expression = only_snowflake(metric["expression"])
+ check_metric_expression(expression, dataset_names)
+ if MANY_SIDE not in measures_by_dataset:
+ die(expression)
+ measures_by_dataset[MANY_SIDE].append(
+ {
+ "name": metric["name"],
+ "expression": expression,
+ "description": metric["description"],
+ "id": measure_id(metric["name"]),
+ }
+ )
+
+ zsheets = []
+ tables = []
+ table_metadata = []
+ for dataset in model["datasets"]:
+ uuid = zsheet_uuid(dataset["name"])
+ zsheets.append({"uuid": uuid, "name": dataset["name"], "version": "1"})
+ database, schema, table_name = _split_source(dataset["source"])
+ columns = []
+ primary_key = list(dataset.get("primary_key") or [])
+ for field in dataset["fields"]:
+ expression = only_snowflake(field["expression"])
+ if expression != field["name"]:
+ die(expression)
+ datatype = field["datatype"]
+ if dimension_for_datatype(datatype) != field.get("dimension"):
+ die(field["name"])
+ properties = {"dataType": OSSIE_TO_WISDOM[datatype]}
+ if field["name"] in primary_key:
+ properties["isPrimaryKey"] = True
+ columns.append({"name": field["name"], "properties": properties})
+ zsheet = {
+ "ref": {"uuid": uuid, "name": dataset["name"], "version": "1"},
+ "location": {
+ "database": database,
+ "schema": schema,
+ "dbTable": table_name,
+ "connectionId": CONNECTION_ID,
+ },
+ "primaryKey": {"columns": primary_key},
+ "columns": columns,
+ }
+ if measures_by_dataset[dataset["name"]]:
+ zsheet["measures"] = measures_by_dataset[dataset["name"]]
+ tables.append({"zsheet_uuid": uuid, "zsheet_json": zsheet})
+ table_metadata.append(
+ {
+ "zsheet_uuid": uuid,
+ "connection_id": CONNECTION_ID,
+ "database": database,
+ "schema": schema,
+ "table_name": table_name,
+ }
+ )
+
+ return {
+ "version": "1.0",
+ "export_metadata": {
+ "exported_at": EXPORTED_AT,
+ "source_domain_id": SOURCE_DOMAIN_ID,
+ "domain_name": name,
+ },
+ "domain": {
+ "zsheet_json": {
+ "ref": {"uuid": domain_uuid(name), "name": name, "version": "1"},
+ "description": model["description"],
+ "zsheetType": "DOMAIN",
+ "domainSystemInstructions": model["description"],
+ "knowledge": _knowledge_from_model(model),
+ "relationshipGraph": {
+ "zsheets": zsheets,
+ "relationships": _ossie_relationships(model),
+ },
+ }
+ },
+ "tables": tables,
+ "connections": [
+ {
+ "connection_id": CONNECTION_ID,
+ "dialect": "snowflake",
+ "name": "Snowflake",
+ }
+ ],
+ "reviewed_queries": empty_item_list(),
+ "synonym_sets": empty_item_list(),
+ "table_metadata": table_metadata,
+ "recommended_questions": [],
+ }
+
+
+def wisdom_to_ossie(export: dict) -> dict:
+ reject_nonempty_synonym_sets(export)
+ if export.get("version") != "1.0":
+ die(str(export.get("version")))
+ metadata = export.get("export_metadata") or {}
+ if metadata.get("exported_at") != EXPORTED_AT:
+ die("exported_at")
+ if metadata.get("source_domain_id") != SOURCE_DOMAIN_ID:
+ die("source_domain_id")
+ domain = export["domain"]["zsheet_json"]
+ name = domain["ref"]["name"]
+ if metadata.get("domain_name") != name:
+ die("domain_name")
+ if domain.get("domainSystemInstructions") != domain.get("description"):
+ die("domainSystemInstructions")
+ if domain["ref"].get("uuid") != domain_uuid(name):
+ die(domain["ref"].get("uuid", "uuid"))
+
+ dataset_names = [table["zsheet_json"]["ref"]["name"] for table in export["tables"]]
+ datasets = []
+ metrics: list[dict] = []
+ for table in export["tables"]:
+ zsheet = table["zsheet_json"]
+ if zsheet.get("formulas"):
+ die("formulas")
+ dataset = _wisdom_table_to_dataset(zsheet)
+ datasets.append(dataset)
+ if zsheet["ref"]["name"] != MANY_SIDE and zsheet.get("measures"):
+ die("measures")
+ if zsheet["ref"]["name"] == MANY_SIDE:
+ for measure in zsheet.get("measures") or []:
+ expression = measure.get("expression")
+ if not isinstance(expression, str):
+ die("expression")
+ check_metric_expression(expression, dataset_names)
+ metrics.append(
+ {
+ "name": measure["name"],
+ "description": measure["description"],
+ "expression": snowflake_expression(expression),
+ }
+ )
+
+ instructions = None
+ for item in domain.get("knowledge") or []:
+ title = item.get("name", "")
+ content = item.get("content", "")
+ if title == BOARD_REVENUE:
+ if instructions is not None:
+ die(title)
+ instructions = content
+ continue
+ if title.startswith(SYNONYM_PREFIX):
+ metric_name = title[len(SYNONYM_PREFIX) :]
+ metric = next((item for item in metrics if item["name"] == metric_name), None)
+ if metric is None:
+ die(title)
+ if "ai_context" in metric:
+ die(title)
+ metric["ai_context"] = {"synonyms": _parse_bullets(content)}
+ continue
+ die(title or "knowledge")
+
+ model = {
+ "version": SPEC_VERSION,
+ "name": name,
+ "description": domain["description"],
+ }
+ if instructions:
+ model["ai_context"] = {"instructions": instructions}
+ model["datasets"] = datasets
+ model["relationships"] = _wisdom_relationships(domain)
+ model["metrics"] = metrics
+ return model
+
+
+def _knowledge_from_model(model: dict) -> list[dict]:
+ items = []
+ for metric in model.get("metrics") or []:
+ synonyms = (metric.get("ai_context") or {}).get("synonyms")
+ if not synonyms:
+ continue
+ title = f"{SYNONYM_PREFIX}{metric['name']}"
+ items.append(
+ {
+ "name": title,
+ "content": "\n".join(f"- {synonym}" for synonym in synonyms),
+ "id": knowledge_id(title),
+ }
+ )
+ instructions = (model.get("ai_context") or {}).get("instructions") if isinstance(model.get("ai_context"), dict) else None
+ if instructions:
+ items.append(
+ {
+ "name": BOARD_REVENUE,
+ "content": instructions,
+ "id": knowledge_id(BOARD_REVENUE),
+ }
+ )
+ reject_duplicate_knowledge_ids(items)
+ return items
+
+
+def _ossie_relationships(model: dict) -> list[dict]:
+ edges = []
+ for relationship in model.get("relationships") or []:
+ if relationship.get("from") != MANY_SIDE or relationship.get("to") != ONE_SIDE:
+ die(str(relationship.get("name")))
+ from_columns = relationship.get("from_columns") or []
+ to_columns = relationship.get("to_columns") or []
+ if len(from_columns) != 1 or len(to_columns) != 1:
+ die("relationship_columns")
+ left_ref = {"uuid": zsheet_uuid(relationship["from"]), "name": relationship["from"]}
+ right_ref = {"uuid": zsheet_uuid(relationship["to"]), "name": relationship["to"]}
+ edges.append(
+ {
+ "properties": {
+ "joinCondition": {
+ "leftColumn": {"name": from_columns[0], "zsheetRef": left_ref},
+ "rightColumn": {"name": to_columns[0], "zsheetRef": right_ref},
+ },
+ "relationshipType": "MANY_TO_ONE",
+ },
+ "leftDataSource": {"zsheet": left_ref},
+ "rightDataSource": {"zsheet": right_ref},
+ }
+ )
+ if len(edges) != 1:
+ die("relationships")
+ return edges
+
+
+def _wisdom_relationships(domain: dict) -> list[dict]:
+ edges = domain.get("relationshipGraph", {}).get("relationships") or []
+ if len(edges) != 1:
+ die("relationships")
+ properties = edges[0].get("properties") or {}
+ if "compoundJoinCondition" in properties:
+ die("compoundJoinCondition")
+ if properties.get("relationshipType") != "MANY_TO_ONE":
+ die(str(properties.get("relationshipType", "relationshipType")))
+ join = properties.get("joinCondition")
+ if not isinstance(join, dict):
+ die("joinCondition")
+ left = join["leftColumn"]
+ right = join["rightColumn"]
+ left_name = left["zsheetRef"]["name"]
+ right_name = right["zsheetRef"]["name"]
+ if left_name != MANY_SIDE or right_name != ONE_SIDE:
+ die(left_name)
+ return [
+ {
+ "name": RELATIONSHIP_NAME,
+ "from": left_name,
+ "to": right_name,
+ "from_columns": [left["name"]],
+ "to_columns": [right["name"]],
+ }
+ ]
+
+
+def _wisdom_table_to_dataset(zsheet: dict) -> dict:
+ name = zsheet["ref"]["name"]
+ if zsheet["ref"].get("uuid") != zsheet_uuid(name):
+ die(zsheet["ref"].get("uuid", "uuid"))
+ location = zsheet["location"]
+ if location.get("connectionId") != CONNECTION_ID:
+ die(str(location.get("connectionId", "connectionId")))
+ fields = []
+ for column in zsheet.get("columns") or []:
+ data_type = (column.get("properties") or {}).get("dataType")
+ if data_type not in WISDOM_TO_OSSIE:
+ die(str(data_type))
+ datatype = WISDOM_TO_OSSIE[data_type]
+ expression = column["name"]
+ check_expression(expression)
+ if not isinstance(expression, str):
+ die("expression")
+ field = {
+ "name": column["name"],
+ "expression": snowflake_expression(expression),
+ "datatype": datatype,
+ }
+ dimension = dimension_for_datatype(datatype)
+ if dimension is not None:
+ field["dimension"] = dimension
+ fields.append(field)
+ primary_key = (zsheet.get("primaryKey") or {}).get("columns")
+ if not isinstance(primary_key, list) or not primary_key:
+ die("primary_key")
+ return {
+ "name": name,
+ "source": f"{location['database']}.{location['schema']}.{location['dbTable']}",
+ "primary_key": list(primary_key),
+ "fields": fields,
+ }
+
+
+def _split_source(source: str) -> tuple[str, str, str]:
+ parts = source.split(".")
+ if len(parts) != 3 or not all(parts):
+ die(source)
+ return parts[0], parts[1], parts[2]
+
+
+def _parse_bullets(content: str) -> list[str]:
+ lines = content.splitlines()
+ if not lines:
+ die("synonyms")
+ synonyms = []
+ for line in lines:
+ if not line.startswith("- "):
+ die(line)
+ synonyms.append(line[2:])
+ return synonyms
diff --git a/converters/wisdom-semantic-view/tests/fixtures/finance/ossie/cfo_cockpit.enriched.yaml b/converters/wisdom-semantic-view/tests/fixtures/finance/ossie/cfo_cockpit.enriched.yaml
new file mode 100644
index 00000000..2462acfd
--- /dev/null
+++ b/converters/wisdom-semantic-view/tests/fixtures/finance/ossie/cfo_cockpit.enriched.yaml
@@ -0,0 +1,113 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+version: 0.2.0.dev0
+name: finance_cfo_cockpit
+description: Bookings and the fiscal calendar.
+ai_context:
+ instructions: Board revenue is recognized bookings. It is not cash collected. The fiscal year starts in February.
+datasets:
+- name: bookings
+ source: finance.public.bookings
+ primary_key:
+ - booking_id
+ fields:
+ - name: booking_id
+ expression:
+ dialects:
+ - dialect: SNOWFLAKE
+ expression: booking_id
+ datatype: Integer
+ dimension:
+ is_time: false
+ - name: customer_id
+ expression:
+ dialects:
+ - dialect: SNOWFLAKE
+ expression: customer_id
+ datatype: Integer
+ dimension:
+ is_time: false
+ - name: close_date
+ expression:
+ dialects:
+ - dialect: SNOWFLAKE
+ expression: close_date
+ datatype: Date
+ dimension:
+ is_time: true
+ - name: amount
+ expression:
+ dialects:
+ - dialect: SNOWFLAKE
+ expression: amount
+ datatype: Decimal
+- name: fiscal_calendar
+ source: finance.public.fiscal_calendar
+ primary_key:
+ - fiscal_date
+ fields:
+ - name: fiscal_year
+ expression:
+ dialects:
+ - dialect: SNOWFLAKE
+ expression: fiscal_year
+ datatype: Integer
+ dimension:
+ is_time: false
+ - name: fiscal_quarter
+ expression:
+ dialects:
+ - dialect: SNOWFLAKE
+ expression: fiscal_quarter
+ datatype: String
+ dimension:
+ is_time: false
+ - name: fiscal_date
+ expression:
+ dialects:
+ - dialect: SNOWFLAKE
+ expression: fiscal_date
+ datatype: Date
+ dimension:
+ is_time: true
+relationships:
+- name: bookings_to_fiscal_calendar
+ from: bookings
+ to: fiscal_calendar
+ from_columns:
+ - close_date
+ to_columns:
+ - fiscal_date
+metrics:
+- name: booking_amount
+ description: Sum of booking amount
+ expression:
+ dialects:
+ - dialect: SNOWFLAKE
+ expression: SUM(bookings.amount)
+- name: revenue
+ description: 'Board revenue: recognized bookings. Not cash collected.'
+ expression:
+ dialects:
+ - dialect: SNOWFLAKE
+ expression: SUM(bookings.amount)
+ ai_context:
+ synonyms:
+ - revenue
+ - net revenue
+ - board revenue
diff --git a/converters/wisdom-semantic-view/tests/fixtures/finance/ossie/cfo_cockpit.yaml b/converters/wisdom-semantic-view/tests/fixtures/finance/ossie/cfo_cockpit.yaml
new file mode 100644
index 00000000..7e460550
--- /dev/null
+++ b/converters/wisdom-semantic-view/tests/fixtures/finance/ossie/cfo_cockpit.yaml
@@ -0,0 +1,100 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+version: 0.2.0.dev0
+name: finance_cfo_cockpit
+description: Bookings and the fiscal calendar.
+datasets:
+- name: bookings
+ source: finance.public.bookings
+ primary_key:
+ - booking_id
+ fields:
+ - name: booking_id
+ expression:
+ dialects:
+ - dialect: SNOWFLAKE
+ expression: booking_id
+ datatype: Integer
+ dimension:
+ is_time: false
+ - name: customer_id
+ expression:
+ dialects:
+ - dialect: SNOWFLAKE
+ expression: customer_id
+ datatype: Integer
+ dimension:
+ is_time: false
+ - name: close_date
+ expression:
+ dialects:
+ - dialect: SNOWFLAKE
+ expression: close_date
+ datatype: Date
+ dimension:
+ is_time: true
+ - name: amount
+ expression:
+ dialects:
+ - dialect: SNOWFLAKE
+ expression: amount
+ datatype: Decimal
+- name: fiscal_calendar
+ source: finance.public.fiscal_calendar
+ primary_key:
+ - fiscal_date
+ fields:
+ - name: fiscal_year
+ expression:
+ dialects:
+ - dialect: SNOWFLAKE
+ expression: fiscal_year
+ datatype: Integer
+ dimension:
+ is_time: false
+ - name: fiscal_quarter
+ expression:
+ dialects:
+ - dialect: SNOWFLAKE
+ expression: fiscal_quarter
+ datatype: String
+ dimension:
+ is_time: false
+ - name: fiscal_date
+ expression:
+ dialects:
+ - dialect: SNOWFLAKE
+ expression: fiscal_date
+ datatype: Date
+ dimension:
+ is_time: true
+relationships:
+- name: bookings_to_fiscal_calendar
+ from: bookings
+ to: fiscal_calendar
+ from_columns:
+ - close_date
+ to_columns:
+ - fiscal_date
+metrics:
+- name: booking_amount
+ description: Sum of booking amount
+ expression:
+ dialects:
+ - dialect: SNOWFLAKE
+ expression: SUM(bookings.amount)
diff --git a/converters/wisdom-semantic-view/tests/fixtures/finance/snowflake/cfo_cockpit.after.yaml b/converters/wisdom-semantic-view/tests/fixtures/finance/snowflake/cfo_cockpit.after.yaml
new file mode 100644
index 00000000..4c0255dd
--- /dev/null
+++ b/converters/wisdom-semantic-view/tests/fixtures/finance/snowflake/cfo_cockpit.after.yaml
@@ -0,0 +1,82 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+name: finance_cfo_cockpit
+description: Bookings and the fiscal calendar.
+tables:
+- name: bookings
+ base_table:
+ database: finance
+ schema: public
+ table: bookings
+ primary_key:
+ columns:
+ - booking_id
+ dimensions:
+ - name: booking_id
+ expr: booking_id
+ data_type: NUMBER(38,0)
+ - name: customer_id
+ expr: customer_id
+ data_type: NUMBER(38,0)
+ time_dimensions:
+ - name: close_date
+ expr: close_date
+ data_type: DATE
+ facts:
+ - name: amount
+ expr: amount
+ data_type: NUMBER(18,2)
+- name: fiscal_calendar
+ base_table:
+ database: finance
+ schema: public
+ table: fiscal_calendar
+ primary_key:
+ columns:
+ - fiscal_date
+ dimensions:
+ - name: fiscal_year
+ expr: fiscal_year
+ data_type: NUMBER(38,0)
+ - name: fiscal_quarter
+ expr: fiscal_quarter
+ data_type: VARCHAR
+ time_dimensions:
+ - name: fiscal_date
+ expr: fiscal_date
+ data_type: DATE
+relationships:
+- name: bookings_to_fiscal_calendar
+ left_table: bookings
+ right_table: fiscal_calendar
+ relationship_columns:
+ - left_column: close_date
+ right_column: fiscal_date
+metrics:
+- name: booking_amount
+ description: Sum of booking amount
+ expr: SUM(bookings.amount)
+- name: revenue
+ description: 'Board revenue: recognized bookings. Not cash collected.'
+ expr: SUM(bookings.amount)
+ synonyms:
+ - revenue
+ - net revenue
+ - board revenue
+module_custom_instructions:
+ sql_generation: Board revenue is recognized bookings. It is not cash collected. The fiscal year starts in February.
diff --git a/converters/wisdom-semantic-view/tests/fixtures/finance/snowflake/cfo_cockpit.before.yaml b/converters/wisdom-semantic-view/tests/fixtures/finance/snowflake/cfo_cockpit.before.yaml
new file mode 100644
index 00000000..5130844f
--- /dev/null
+++ b/converters/wisdom-semantic-view/tests/fixtures/finance/snowflake/cfo_cockpit.before.yaml
@@ -0,0 +1,73 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+name: finance_cfo_cockpit
+description: Bookings and the fiscal calendar.
+tables:
+- name: bookings
+ base_table:
+ database: finance
+ schema: public
+ table: bookings
+ primary_key:
+ columns:
+ - booking_id
+ dimensions:
+ - name: booking_id
+ expr: booking_id
+ data_type: NUMBER(38,0)
+ - name: customer_id
+ expr: customer_id
+ data_type: NUMBER(38,0)
+ time_dimensions:
+ - name: close_date
+ expr: close_date
+ data_type: DATE
+ facts:
+ - name: amount
+ expr: amount
+ data_type: NUMBER(18,2)
+- name: fiscal_calendar
+ base_table:
+ database: finance
+ schema: public
+ table: fiscal_calendar
+ primary_key:
+ columns:
+ - fiscal_date
+ dimensions:
+ - name: fiscal_year
+ expr: fiscal_year
+ data_type: NUMBER(38,0)
+ - name: fiscal_quarter
+ expr: fiscal_quarter
+ data_type: VARCHAR
+ time_dimensions:
+ - name: fiscal_date
+ expr: fiscal_date
+ data_type: DATE
+relationships:
+- name: bookings_to_fiscal_calendar
+ left_table: bookings
+ right_table: fiscal_calendar
+ relationship_columns:
+ - left_column: close_date
+ right_column: fiscal_date
+metrics:
+- name: booking_amount
+ description: Sum of booking amount
+ expr: SUM(bookings.amount)
diff --git a/converters/wisdom-semantic-view/tests/fixtures/finance/wisdom/cfo_cockpit.enriched.json b/converters/wisdom-semantic-view/tests/fixtures/finance/wisdom/cfo_cockpit.enriched.json
new file mode 100644
index 00000000..16f217ec
--- /dev/null
+++ b/converters/wisdom-semantic-view/tests/fixtures/finance/wisdom/cfo_cockpit.enriched.json
@@ -0,0 +1,219 @@
+{
+ "version": "1.0",
+ "export_metadata": {
+ "exported_at": "2026-01-01T00:00:00+00:00",
+ "source_domain_id": "demo-finance-cfo",
+ "domain_name": "finance_cfo_cockpit"
+ },
+ "domain": {
+ "zsheet_json": {
+ "ref": {
+ "uuid": "ET_DOMAIN_finance_cfo_cockpit",
+ "name": "finance_cfo_cockpit",
+ "version": "1"
+ },
+ "description": "Bookings and the fiscal calendar.",
+ "zsheetType": "DOMAIN",
+ "domainSystemInstructions": "Bookings and the fiscal calendar.",
+ "knowledge": [
+ {
+ "name": "Synonyms for revenue",
+ "content": "- revenue\n- net revenue\n- board revenue",
+ "id": "ET_UNSTRUCTURED_KNOWLEDGE_synonyms_for_revenue"
+ },
+ {
+ "name": "Board revenue",
+ "content": "Board revenue is recognized bookings. It is not cash collected. The fiscal year starts in February.",
+ "id": "ET_UNSTRUCTURED_KNOWLEDGE_board_revenue"
+ }
+ ],
+ "relationshipGraph": {
+ "zsheets": [
+ {
+ "uuid": "ET_ZSHEET_bookings",
+ "name": "bookings",
+ "version": "1"
+ },
+ {
+ "uuid": "ET_ZSHEET_fiscal_calendar",
+ "name": "fiscal_calendar",
+ "version": "1"
+ }
+ ],
+ "relationships": [
+ {
+ "properties": {
+ "joinCondition": {
+ "leftColumn": {
+ "name": "close_date",
+ "zsheetRef": {
+ "uuid": "ET_ZSHEET_bookings",
+ "name": "bookings"
+ }
+ },
+ "rightColumn": {
+ "name": "fiscal_date",
+ "zsheetRef": {
+ "uuid": "ET_ZSHEET_fiscal_calendar",
+ "name": "fiscal_calendar"
+ }
+ }
+ },
+ "relationshipType": "MANY_TO_ONE"
+ },
+ "leftDataSource": {
+ "zsheet": {
+ "uuid": "ET_ZSHEET_bookings",
+ "name": "bookings"
+ }
+ },
+ "rightDataSource": {
+ "zsheet": {
+ "uuid": "ET_ZSHEET_fiscal_calendar",
+ "name": "fiscal_calendar"
+ }
+ }
+ }
+ ]
+ }
+ }
+ },
+ "tables": [
+ {
+ "zsheet_uuid": "ET_ZSHEET_bookings",
+ "zsheet_json": {
+ "ref": {
+ "uuid": "ET_ZSHEET_bookings",
+ "name": "bookings",
+ "version": "1"
+ },
+ "location": {
+ "database": "finance",
+ "schema": "public",
+ "dbTable": "bookings",
+ "connectionId": "et-connection-snowflake"
+ },
+ "primaryKey": {
+ "columns": [
+ "booking_id"
+ ]
+ },
+ "columns": [
+ {
+ "name": "booking_id",
+ "properties": {
+ "dataType": "INT64",
+ "isPrimaryKey": true
+ }
+ },
+ {
+ "name": "customer_id",
+ "properties": {
+ "dataType": "INT64"
+ }
+ },
+ {
+ "name": "close_date",
+ "properties": {
+ "dataType": "DATE"
+ }
+ },
+ {
+ "name": "amount",
+ "properties": {
+ "dataType": "DECIMAL"
+ }
+ }
+ ],
+ "measures": [
+ {
+ "name": "booking_amount",
+ "expression": "SUM(bookings.amount)",
+ "description": "Sum of booking amount",
+ "id": "MEASURE_booking_amount"
+ },
+ {
+ "name": "revenue",
+ "expression": "SUM(bookings.amount)",
+ "description": "Board revenue: recognized bookings. Not cash collected.",
+ "id": "MEASURE_revenue"
+ }
+ ]
+ }
+ },
+ {
+ "zsheet_uuid": "ET_ZSHEET_fiscal_calendar",
+ "zsheet_json": {
+ "ref": {
+ "uuid": "ET_ZSHEET_fiscal_calendar",
+ "name": "fiscal_calendar",
+ "version": "1"
+ },
+ "location": {
+ "database": "finance",
+ "schema": "public",
+ "dbTable": "fiscal_calendar",
+ "connectionId": "et-connection-snowflake"
+ },
+ "primaryKey": {
+ "columns": [
+ "fiscal_date"
+ ]
+ },
+ "columns": [
+ {
+ "name": "fiscal_year",
+ "properties": {
+ "dataType": "INT64"
+ }
+ },
+ {
+ "name": "fiscal_quarter",
+ "properties": {
+ "dataType": "VARCHAR"
+ }
+ },
+ {
+ "name": "fiscal_date",
+ "properties": {
+ "dataType": "DATE",
+ "isPrimaryKey": true
+ }
+ }
+ ]
+ }
+ }
+ ],
+ "connections": [
+ {
+ "connection_id": "et-connection-snowflake",
+ "dialect": "snowflake",
+ "name": "Snowflake"
+ }
+ ],
+ "reviewed_queries": {
+ "ref": null,
+ "items_json": "{}"
+ },
+ "synonym_sets": {
+ "ref": null,
+ "items_json": "{}"
+ },
+ "table_metadata": [
+ {
+ "zsheet_uuid": "ET_ZSHEET_bookings",
+ "connection_id": "et-connection-snowflake",
+ "database": "finance",
+ "schema": "public",
+ "table_name": "bookings"
+ },
+ {
+ "zsheet_uuid": "ET_ZSHEET_fiscal_calendar",
+ "connection_id": "et-connection-snowflake",
+ "database": "finance",
+ "schema": "public",
+ "table_name": "fiscal_calendar"
+ }
+ ],
+ "recommended_questions": []
+}
diff --git a/converters/wisdom-semantic-view/tests/fixtures/finance/wisdom/cfo_cockpit.imported.json b/converters/wisdom-semantic-view/tests/fixtures/finance/wisdom/cfo_cockpit.imported.json
new file mode 100644
index 00000000..b9d0e68b
--- /dev/null
+++ b/converters/wisdom-semantic-view/tests/fixtures/finance/wisdom/cfo_cockpit.imported.json
@@ -0,0 +1,202 @@
+{
+ "version": "1.0",
+ "export_metadata": {
+ "exported_at": "2026-01-01T00:00:00+00:00",
+ "source_domain_id": "demo-finance-cfo",
+ "domain_name": "finance_cfo_cockpit"
+ },
+ "domain": {
+ "zsheet_json": {
+ "ref": {
+ "uuid": "ET_DOMAIN_finance_cfo_cockpit",
+ "name": "finance_cfo_cockpit",
+ "version": "1"
+ },
+ "description": "Bookings and the fiscal calendar.",
+ "zsheetType": "DOMAIN",
+ "domainSystemInstructions": "Bookings and the fiscal calendar.",
+ "knowledge": [],
+ "relationshipGraph": {
+ "zsheets": [
+ {
+ "uuid": "ET_ZSHEET_bookings",
+ "name": "bookings",
+ "version": "1"
+ },
+ {
+ "uuid": "ET_ZSHEET_fiscal_calendar",
+ "name": "fiscal_calendar",
+ "version": "1"
+ }
+ ],
+ "relationships": [
+ {
+ "properties": {
+ "joinCondition": {
+ "leftColumn": {
+ "name": "close_date",
+ "zsheetRef": {
+ "uuid": "ET_ZSHEET_bookings",
+ "name": "bookings"
+ }
+ },
+ "rightColumn": {
+ "name": "fiscal_date",
+ "zsheetRef": {
+ "uuid": "ET_ZSHEET_fiscal_calendar",
+ "name": "fiscal_calendar"
+ }
+ }
+ },
+ "relationshipType": "MANY_TO_ONE"
+ },
+ "leftDataSource": {
+ "zsheet": {
+ "uuid": "ET_ZSHEET_bookings",
+ "name": "bookings"
+ }
+ },
+ "rightDataSource": {
+ "zsheet": {
+ "uuid": "ET_ZSHEET_fiscal_calendar",
+ "name": "fiscal_calendar"
+ }
+ }
+ }
+ ]
+ }
+ }
+ },
+ "tables": [
+ {
+ "zsheet_uuid": "ET_ZSHEET_bookings",
+ "zsheet_json": {
+ "ref": {
+ "uuid": "ET_ZSHEET_bookings",
+ "name": "bookings",
+ "version": "1"
+ },
+ "location": {
+ "database": "finance",
+ "schema": "public",
+ "dbTable": "bookings",
+ "connectionId": "et-connection-snowflake"
+ },
+ "primaryKey": {
+ "columns": [
+ "booking_id"
+ ]
+ },
+ "columns": [
+ {
+ "name": "booking_id",
+ "properties": {
+ "dataType": "INT64",
+ "isPrimaryKey": true
+ }
+ },
+ {
+ "name": "customer_id",
+ "properties": {
+ "dataType": "INT64"
+ }
+ },
+ {
+ "name": "close_date",
+ "properties": {
+ "dataType": "DATE"
+ }
+ },
+ {
+ "name": "amount",
+ "properties": {
+ "dataType": "DECIMAL"
+ }
+ }
+ ],
+ "measures": [
+ {
+ "name": "booking_amount",
+ "expression": "SUM(bookings.amount)",
+ "description": "Sum of booking amount",
+ "id": "MEASURE_booking_amount"
+ }
+ ]
+ }
+ },
+ {
+ "zsheet_uuid": "ET_ZSHEET_fiscal_calendar",
+ "zsheet_json": {
+ "ref": {
+ "uuid": "ET_ZSHEET_fiscal_calendar",
+ "name": "fiscal_calendar",
+ "version": "1"
+ },
+ "location": {
+ "database": "finance",
+ "schema": "public",
+ "dbTable": "fiscal_calendar",
+ "connectionId": "et-connection-snowflake"
+ },
+ "primaryKey": {
+ "columns": [
+ "fiscal_date"
+ ]
+ },
+ "columns": [
+ {
+ "name": "fiscal_year",
+ "properties": {
+ "dataType": "INT64"
+ }
+ },
+ {
+ "name": "fiscal_quarter",
+ "properties": {
+ "dataType": "VARCHAR"
+ }
+ },
+ {
+ "name": "fiscal_date",
+ "properties": {
+ "dataType": "DATE",
+ "isPrimaryKey": true
+ }
+ }
+ ]
+ }
+ }
+ ],
+ "connections": [
+ {
+ "connection_id": "et-connection-snowflake",
+ "dialect": "snowflake",
+ "name": "Snowflake"
+ }
+ ],
+ "reviewed_queries": {
+ "ref": null,
+ "items_json": "{}"
+ },
+ "synonym_sets": {
+ "ref": null,
+ "items_json": "{}"
+ },
+ "table_metadata": [
+ {
+ "zsheet_uuid": "ET_ZSHEET_bookings",
+ "connection_id": "et-connection-snowflake",
+ "database": "finance",
+ "schema": "public",
+ "table_name": "bookings"
+ },
+ {
+ "zsheet_uuid": "ET_ZSHEET_fiscal_calendar",
+ "connection_id": "et-connection-snowflake",
+ "database": "finance",
+ "schema": "public",
+ "table_name": "fiscal_calendar"
+ }
+ ],
+ "recommended_questions": []
+}
diff --git a/converters/wisdom-semantic-view/tests/fixtures/finance/wisdom/enrichment.yaml b/converters/wisdom-semantic-view/tests/fixtures/finance/wisdom/enrichment.yaml
new file mode 100644
index 00000000..969379fa
--- /dev/null
+++ b/converters/wisdom-semantic-view/tests/fixtures/finance/wisdom/enrichment.yaml
@@ -0,0 +1,29 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+metric:
+ name: revenue
+ expr: SUM(bookings.amount)
+ description: 'Board revenue: recognized bookings. Not cash collected.'
+knowledge:
+- name: Synonyms for revenue
+ content: |-
+ - revenue
+ - net revenue
+ - board revenue
+- name: Board revenue
+ content: Board revenue is recognized bookings. It is not cash collected. The fiscal year starts in February.
diff --git a/converters/wisdom-semantic-view/tests/test_packaging.py b/converters/wisdom-semantic-view/tests/test_packaging.py
new file mode 100644
index 00000000..0637618e
--- /dev/null
+++ b/converters/wisdom-semantic-view/tests/test_packaging.py
@@ -0,0 +1,62 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+"""Packaging invariants that upstream's PR checklist gates."""
+
+from pathlib import Path
+
+import ossie_wisdom_semantic_view
+
+ROOT = Path(__file__).resolve().parents[1]
+LICENSE_MARKER = "Licensed to the Apache Software Foundation (ASF)"
+
+
+def test_package_exposes_a_version():
+ assert ossie_wisdom_semantic_view.__version__ == "0.1.0"
+
+
+def test_every_source_file_carries_the_asf_header():
+ sources = [
+ *(ROOT / "src").rglob("*.py"),
+ *(ROOT / "tests").rglob("*.py"),
+ ]
+ assert sources, "expected at least one source file"
+ missing = [
+ str(path.relative_to(ROOT))
+ for path in sources
+ if LICENSE_MARKER not in path.read_text(encoding="utf-8")
+ ]
+ assert missing == [], f"ASF header missing from: {missing}"
+
+
+def test_non_python_packaging_files_carry_the_asf_header():
+ repo_root = ROOT.parent.parent
+ files = {
+ "pyproject.toml": ROOT / "pyproject.toml",
+ ".gitignore": ROOT / ".gitignore",
+ "README.md": ROOT / "README.md",
+ "CI workflow": repo_root / ".github" / "workflows" / "converter-wisdom-semantic-view-ci.yml",
+ }
+ yaml_fixtures = list((ROOT / "tests" / "fixtures").rglob("*.yaml"))
+ assert yaml_fixtures, "expected YAML fixtures"
+ files.update({str(path.relative_to(ROOT)): path for path in yaml_fixtures})
+ missing = [
+ label
+ for label, path in files.items()
+ if not path.is_file() or LICENSE_MARKER not in path.read_text(encoding="utf-8")
+ ]
+ assert missing == [], f"ASF header missing from: {missing}"
diff --git a/converters/wisdom-semantic-view/tests/test_round_trip.py b/converters/wisdom-semantic-view/tests/test_round_trip.py
new file mode 100644
index 00000000..8e8424b4
--- /dev/null
+++ b/converters/wisdom-semantic-view/tests/test_round_trip.py
@@ -0,0 +1,314 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+import json
+from pathlib import Path
+
+import jsonschema
+import pytest
+import yaml
+
+from ossie_wisdom_semantic_view.cli import main
+from ossie_wisdom_semantic_view.enrich import apply_enrichment, load_enrichment
+from ossie_wisdom_semantic_view.model import dump_ossie, load_ossie
+from ossie_wisdom_semantic_view.pipeline import convert_finance
+from ossie_wisdom_semantic_view.semantic_view import (
+ dump_semantic_view,
+ load_semantic_view,
+ ossie_to_semantic_view,
+ semantic_view_to_ossie,
+)
+from ossie_wisdom_semantic_view.wisdom import dump_wisdom, load_wisdom, ossie_to_wisdom, wisdom_to_ossie
+
+ROOT = Path(__file__).resolve().parents[1]
+FINANCE = ROOT / "tests" / "fixtures" / "finance"
+REPO = ROOT.parent.parent
+_HEADER_END = "# under the License.\n"
+
+
+def fixture_body(path: Path) -> str:
+ text = path.read_text()
+ if path.suffix == ".json" or not text.startswith("# Licensed to the Apache Software Foundation"):
+ return text
+ marker = text.find(_HEADER_END)
+ assert marker != -1, path
+ body = text[marker + len(_HEADER_END) :]
+ if body.startswith("\n"):
+ body = body[1:]
+ return body
+
+
+def test_before_file_round_trips_without_enrichment():
+ text = fixture_body(FINANCE / "snowflake" / "cfo_cockpit.before.yaml")
+ view = load_semantic_view(text)
+ again = dump_semantic_view(ossie_to_semantic_view(semantic_view_to_ossie(view)))
+ assert again == text
+
+
+def test_pipeline_matches_goldens():
+ artifacts = convert_finance(
+ fixture_body(FINANCE / "snowflake" / "cfo_cockpit.before.yaml"),
+ fixture_body(FINANCE / "wisdom" / "enrichment.yaml"),
+ )
+ assert artifacts.ossie_yaml == fixture_body(FINANCE / "ossie" / "cfo_cockpit.yaml")
+ assert artifacts.ossie_enriched_yaml == fixture_body(FINANCE / "ossie" / "cfo_cockpit.enriched.yaml")
+ assert artifacts.wisdom_imported_json == (FINANCE / "wisdom" / "cfo_cockpit.imported.json").read_text()
+ assert artifacts.wisdom_enriched_json == (FINANCE / "wisdom" / "cfo_cockpit.enriched.json").read_text()
+ assert artifacts.semantic_after_yaml == fixture_body(FINANCE / "snowflake" / "cfo_cockpit.after.yaml")
+
+
+def test_enriched_wisdom_round_trips_through_ossie():
+ artifacts = convert_finance(
+ fixture_body(FINANCE / "snowflake" / "cfo_cockpit.before.yaml"),
+ fixture_body(FINANCE / "wisdom" / "enrichment.yaml"),
+ )
+ enriched = load_wisdom(artifacts.wisdom_enriched_json)
+ assert dump_wisdom(ossie_to_wisdom(wisdom_to_ossie(enriched))) == artifacts.wisdom_enriched_json
+ assert dump_ossie(wisdom_to_ossie(enriched)) == artifacts.ossie_enriched_yaml
+
+
+def test_enrichment_survives_the_wisdom_side():
+ artifacts = convert_finance(
+ fixture_body(FINANCE / "snowflake" / "cfo_cockpit.before.yaml"),
+ fixture_body(FINANCE / "wisdom" / "enrichment.yaml"),
+ )
+ after = yaml.safe_load(artifacts.semantic_after_yaml)
+ revenue = next(metric for metric in after["metrics"] if metric["name"] == "revenue")
+ assert revenue["expr"] == "SUM(bookings.amount)"
+ assert revenue["synonyms"] == ["revenue", "net revenue", "board revenue"]
+ assert after["module_custom_instructions"]["sql_generation"] == (
+ "Board revenue is recognized bookings. It is not cash collected. "
+ "The fiscal year starts in February."
+ )
+ booking_amount = next(metric for metric in after["metrics"] if metric["name"] == "booking_amount")
+ assert "synonyms" not in booking_amount
+ knowledge = json.loads(artifacts.wisdom_enriched_json)["domain"]["zsheet_json"]["knowledge"]
+ assert [item["name"] for item in knowledge] == ["Synonyms for revenue", "Board revenue"]
+ assert "BOOKINGS" not in artifacts.semantic_after_yaml
+
+
+def test_ossie_goldens_match_schema():
+ schema = json.loads((REPO / "core-spec" / "ossie-schema.json").read_text())
+ validator = jsonschema.Draft202012Validator(schema)
+ for name in ("cfo_cockpit.yaml", "cfo_cockpit.enriched.yaml"):
+ document = load_ossie((FINANCE / "ossie" / name).read_text())
+ validator.validate(document)
+ assert document["version"] == "0.2.0.dev0"
+
+
+def test_synonym_sets_stay_empty():
+ artifacts = convert_finance(
+ fixture_body(FINANCE / "snowflake" / "cfo_cockpit.before.yaml"),
+ fixture_body(FINANCE / "wisdom" / "enrichment.yaml"),
+ )
+ for text in (artifacts.wisdom_imported_json, artifacts.wisdom_enriched_json):
+ export = json.loads(text)
+ assert export["synonym_sets"] == {"ref": None, "items_json": "{}"}
+
+
+def test_imported_knowledge_is_empty_and_enrichment_adds_the_metric():
+ artifacts = convert_finance(
+ fixture_body(FINANCE / "snowflake" / "cfo_cockpit.before.yaml"),
+ fixture_body(FINANCE / "wisdom" / "enrichment.yaml"),
+ )
+ imported = json.loads(artifacts.wisdom_imported_json)
+ enriched = json.loads(artifacts.wisdom_enriched_json)
+ assert imported["domain"]["zsheet_json"]["knowledge"] == []
+ measures = enriched["tables"][0]["zsheet_json"]["measures"]
+ assert [item["name"] for item in measures] == ["booking_amount", "revenue"]
+
+
+def test_other_relationship_is_not_inferred(capsys):
+ view = load_semantic_view(fixture_body(FINANCE / "snowflake" / "cfo_cockpit.before.yaml"))
+ view["relationships"][0]["left_table"] = "fiscal_calendar"
+ view["relationships"][0]["right_table"] = "bookings"
+ with pytest.raises(SystemExit) as caught:
+ semantic_view_to_ossie(view)
+ assert caught.value.code == 1
+ assert capsys.readouterr().err.strip() == "bookings_to_fiscal_calendar"
+
+
+def test_unknown_key_exits(capsys):
+ view = load_semantic_view(fixture_body(FINANCE / "snowflake" / "cfo_cockpit.before.yaml"))
+ view["verified_queries"] = []
+ with pytest.raises(SystemExit) as caught:
+ semantic_view_to_ossie(view)
+ assert caught.value.code == 1
+ assert capsys.readouterr().err.strip() == "verified_queries"
+
+
+def test_unsupported_expression_exits(capsys):
+ view = load_semantic_view(fixture_body(FINANCE / "snowflake" / "cfo_cockpit.before.yaml"))
+ view["metrics"][0]["expr"] = "AVG(bookings.amount)"
+ with pytest.raises(SystemExit) as caught:
+ semantic_view_to_ossie(view)
+ assert caught.value.code == 1
+ assert "AVG(bookings.amount)" in capsys.readouterr().err
+
+
+def test_other_data_type_exits(capsys):
+ view = load_semantic_view(fixture_body(FINANCE / "snowflake" / "cfo_cockpit.before.yaml"))
+ view["tables"][0]["facts"][0]["data_type"] = "FLOAT"
+ with pytest.raises(SystemExit) as caught:
+ semantic_view_to_ossie(view)
+ assert caught.value.code == 1
+ assert capsys.readouterr().err.strip() == "FLOAT"
+
+
+def test_enrichment_rejects_preexisting_sql_generation(capsys):
+ view = load_semantic_view(fixture_body(FINANCE / "snowflake" / "cfo_cockpit.before.yaml"))
+ view["module_custom_instructions"] = {
+ "sql_generation": "Board revenue is recognized bookings."
+ }
+ export = ossie_to_wisdom(semantic_view_to_ossie(view))
+ assert [item["name"] for item in export["domain"]["zsheet_json"]["knowledge"]] == ["Board revenue"]
+ enrichment = load_enrichment(fixture_body(FINANCE / "wisdom" / "enrichment.yaml"))
+ with pytest.raises(SystemExit) as caught:
+ apply_enrichment(export, enrichment)
+ assert caught.value.code == 1
+ assert capsys.readouterr().err.strip() == "Board revenue already exists"
+ assert [item["name"] for item in export["domain"]["zsheet_json"]["knowledge"]] == ["Board revenue"]
+
+
+def test_null_synonym_sets_are_empty_and_non_dict_items_exit(capsys):
+ artifacts = convert_finance(
+ fixture_body(FINANCE / "snowflake" / "cfo_cockpit.before.yaml"),
+ fixture_body(FINANCE / "wisdom" / "enrichment.yaml"),
+ )
+ original = load_wisdom(artifacts.wisdom_imported_json)
+ nulled = load_wisdom(artifacts.wisdom_imported_json)
+ nulled["synonym_sets"] = None
+ assert dump_ossie(wisdom_to_ossie(nulled)) == dump_ossie(wisdom_to_ossie(original))
+
+ listed = load_wisdom(artifacts.wisdom_imported_json)
+ listed["synonym_sets"]["items_json"] = "[]"
+ with pytest.raises(SystemExit) as caught:
+ wisdom_to_ossie(listed)
+ assert caught.value.code == 1
+ assert capsys.readouterr().err.strip() == "synonym_sets.items_json"
+
+
+def test_newline_synonym_exits(capsys):
+ view = load_semantic_view(fixture_body(FINANCE / "snowflake" / "cfo_cockpit.before.yaml"))
+ view["metrics"][0]["synonyms"] = ["board\nrevenue"]
+ with pytest.raises(SystemExit) as caught:
+ semantic_view_to_ossie(view)
+ assert caught.value.code == 1
+ assert capsys.readouterr().err.strip() == "synonyms"
+
+
+def test_duplicate_synonym_exits(capsys):
+ view = load_semantic_view(fixture_body(FINANCE / "snowflake" / "cfo_cockpit.before.yaml"))
+ view["metrics"][0]["synonyms"] = ["revenue", "revenue"]
+ with pytest.raises(SystemExit) as caught:
+ semantic_view_to_ossie(view)
+ assert caught.value.code == 1
+ assert capsys.readouterr().err.strip() == "synonyms"
+
+
+def test_empty_synonyms_are_absent_after_import():
+ original = fixture_body(FINANCE / "snowflake" / "cfo_cockpit.before.yaml")
+ view = load_semantic_view(original)
+ view["metrics"][0]["synonyms"] = []
+ model = semantic_view_to_ossie(view)
+ assert "ai_context" not in model["metrics"][0]
+ assert model == semantic_view_to_ossie(load_semantic_view(original))
+ assert dump_semantic_view(ossie_to_semantic_view(model)) == original
+
+
+def test_knowledge_id_collision_exits(capsys):
+ view = load_semantic_view(fixture_body(FINANCE / "snowflake" / "cfo_cockpit.before.yaml"))
+ model = semantic_view_to_ossie(view)
+ expression = model["metrics"][0]["expression"]
+ model["metrics"] = [
+ {
+ "name": "net-revenue",
+ "description": "Net revenue",
+ "expression": expression,
+ "ai_context": {"synonyms": ["net revenue"]},
+ },
+ {
+ "name": "net_revenue",
+ "description": "Net revenue underscore",
+ "expression": expression,
+ "ai_context": {"synonyms": ["net_revenue"]},
+ },
+ ]
+ with pytest.raises(SystemExit) as caught:
+ ossie_to_wisdom(model)
+ assert caught.value.code == 1
+ assert capsys.readouterr().err.strip() == (
+ "duplicate knowledge id: ET_UNSTRUCTURED_KNOWLEDGE_synonyms_for_net_revenue "
+ "(Synonyms for net-revenue, Synonyms for net_revenue)"
+ )
+
+
+def test_nonempty_synonym_sets_exit(capsys):
+ artifacts = convert_finance(
+ fixture_body(FINANCE / "snowflake" / "cfo_cockpit.before.yaml"),
+ fixture_body(FINANCE / "wisdom" / "enrichment.yaml"),
+ )
+ export = load_wisdom(artifacts.wisdom_imported_json)
+ export["synonym_sets"]["items_json"] = '{"synonyms":["revenue"]}'
+ with pytest.raises(SystemExit) as caught:
+ wisdom_to_ossie(export)
+ assert caught.value.code == 1
+ assert capsys.readouterr().err.strip() == "synonym_sets.items_json"
+
+
+def test_cli_both_directions(tmp_path):
+ domain = tmp_path / "domain.json"
+ assert main(
+ [
+ "to-wisdom",
+ str(FINANCE / "snowflake" / "cfo_cockpit.before.yaml"),
+ "-o",
+ str(domain),
+ "--enrich",
+ str(FINANCE / "wisdom" / "enrichment.yaml"),
+ ]
+ ) == 0
+ assert domain.read_text() == (FINANCE / "wisdom" / "cfo_cockpit.enriched.json").read_text()
+ written = tmp_path / "after.yaml"
+ assert main(["to-semantic-view", str(domain), "-o", str(written)]) == 0
+ assert written.read_text() == fixture_body(FINANCE / "snowflake" / "cfo_cockpit.after.yaml")
+
+
+def test_cli_unknown_key_exits(tmp_path, capsys):
+ view = load_semantic_view(fixture_body(FINANCE / "snowflake" / "cfo_cockpit.before.yaml"))
+ view["tags"] = ["board"]
+ source = tmp_path / "view.yaml"
+ source.write_text(dump_semantic_view(view))
+ with pytest.raises(SystemExit) as caught:
+ main(["to-wisdom", str(source), "-o", str(tmp_path / "domain.json")])
+ assert caught.value.code == 1
+ assert capsys.readouterr().err.strip() == "tags"
+
+
+def test_runtime_dependencies_are_pyyaml_only():
+ project = (ROOT / "pyproject.toml").read_text()
+ dependencies = project.split("dependencies = [", 1)[1].split("]", 1)[0]
+ assert "PyYAML" in dependencies
+ assert "ossie" not in dependencies.lower()
+
+
+def test_package_does_not_call_the_other_spokes():
+ sources = "\n".join(path.read_text() for path in (ROOT / "src").rglob("*.py"))
+ assert "ossie_snowflake" not in sources
+ assert "from ossie_wisdom " not in sources
+ assert "import ossie_wisdom\n" not in sources
+ assert "subprocess" not in sources
diff --git a/converters/wisdom-semantic-view/uv.lock b/converters/wisdom-semantic-view/uv.lock
new file mode 100644
index 00000000..7fe6c38d
--- /dev/null
+++ b/converters/wisdom-semantic-view/uv.lock
@@ -0,0 +1,324 @@
+version = 1
+revision = 3
+requires-python = ">=3.11"
+
+[[package]]
+name = "apache-ossie-wisdom-semantic-view"
+version = "0.1.0"
+source = { editable = "." }
+dependencies = [
+ { name = "pyyaml" },
+]
+
+[package.dev-dependencies]
+dev = [
+ { name = "jsonschema" },
+ { name = "pytest" },
+]
+
+[package.metadata]
+requires-dist = [{ name = "pyyaml", specifier = ">=6.0" }]
+
+[package.metadata.requires-dev]
+dev = [
+ { name = "jsonschema", specifier = ">=4.18" },
+ { name = "pytest", specifier = ">=8.0" },
+]
+
+[[package]]
+name = "attrs"
+version = "26.1.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/9a/8e/82a0fe20a541c03148528be8cac2408564a6c9a0cc7e9171802bc1d26985/attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32", size = 952055, upload-time = "2026-03-19T14:22:25.026Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548, upload-time = "2026-03-19T14:22:23.645Z" },
+]
+
+[[package]]
+name = "colorama"
+version = "0.4.6"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
+]
+
+[[package]]
+name = "iniconfig"
+version = "2.3.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
+]
+
+[[package]]
+name = "jsonschema"
+version = "4.26.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "attrs" },
+ { name = "jsonschema-specifications" },
+ { name = "referencing" },
+ { name = "rpds-py" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/b3/fc/e067678238fa451312d4c62bf6e6cf5ec56375422aee02f9cb5f909b3047/jsonschema-4.26.0.tar.gz", hash = "sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326", size = 366583, upload-time = "2026-01-07T13:41:07.246Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl", hash = "sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce", size = 90630, upload-time = "2026-01-07T13:41:05.306Z" },
+]
+
+[[package]]
+name = "jsonschema-specifications"
+version = "2025.9.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "referencing" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855, upload-time = "2025-09-08T01:34:59.186Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" },
+]
+
+[[package]]
+name = "packaging"
+version = "26.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/7d/fa/3944b40b07da9ce895c0e6303a5ab7d53da063554f534556b134a54d6093/packaging-26.3.tar.gz", hash = "sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79", size = 313412, upload-time = "2026-08-04T18:15:28.737Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/63/34/ba1c580383c9eada3711951fef0795c80b829a078d72188184bcab9dd527/packaging-26.3-py3-none-any.whl", hash = "sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c", size = 129956, upload-time = "2026-08-04T18:15:27.159Z" },
+]
+
+[[package]]
+name = "pluggy"
+version = "1.6.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
+]
+
+[[package]]
+name = "pygments"
+version = "2.21.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/49/2e/ced460408999b33da6b31b0021b0f37d329e202d4169aeb164493778f25b/pygments-2.21.0.tar.gz", hash = "sha256:610ca751c9bc2492b38eb9a38a7fbc93edbbb2d7182edaf34e66ae493dee5c8c", size = 5005329, upload-time = "2026-08-17T08:02:48.824Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/71/46/17f022dd3e953bf20a04a028a21ec746d942f8d2af30fa0f124fa0e6a684/pygments-2.21.0-py3-none-any.whl", hash = "sha256:2363c69b61c4a97c838da3b130dcd6468f4848992b21a82f2a63ec34377137d9", size = 1250147, upload-time = "2026-08-17T08:02:44.912Z" },
+]
+
+[[package]]
+name = "pytest"
+version = "9.1.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "colorama", marker = "sys_platform == 'win32'" },
+ { name = "iniconfig" },
+ { name = "packaging" },
+ { name = "pluggy" },
+ { name = "pygments" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" },
+]
+
+[[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/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" },
+ { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" },
+ { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" },
+ { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" },
+ { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" },
+ { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" },
+ { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" },
+ { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" },
+ { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" },
+ { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" },
+ { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" },
+ { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" },
+ { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" },
+ { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" },
+ { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" },
+ { 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 = "referencing"
+version = "0.37.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "attrs" },
+ { name = "rpds-py" },
+ { name = "typing-extensions", marker = "python_full_version < '3.13'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036, upload-time = "2025-10-13T15:30:48.871Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766, upload-time = "2025-10-13T15:30:47.625Z" },
+]
+
+[[package]]
+name = "rpds-py"
+version = "2026.6.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/aa/2a/9618a122aeb2a169a28b03889a2995fe297588964333d4a7d67bdf46e147/rpds_py-2026.6.3.tar.gz", hash = "sha256:1cebd1337c242e4ec2293e541f712b2da849b29f48f0c293684b71c0632625d4", size = 64051, upload-time = "2026-06-30T07:17:53.009Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/94/1f/a2dca5ffdbf1d475ffc4e80e4d5d720ff3a00f691795910116960ee12511/rpds_py-2026.6.3-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:7b689145a1485c335569bd056464f3243a29af7ed3871c7be31ad624ba239bc7", size = 342174, upload-time = "2026-06-30T07:14:54.821Z" },
+ { url = "https://files.pythonhosted.org/packages/4d/dc/323d08583c0832911768663d1944f0107fcd4088704858d84b5e06d105a0/rpds_py-2026.6.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:db08f45aecde626498fb3df07bcf6d2ec040af42e859a4f5040d79c200342911", size = 345513, upload-time = "2026-06-30T07:14:56.515Z" },
+ { url = "https://files.pythonhosted.org/packages/0b/2a/e31989834d18d2f26ec1d2774c5b1eb3331df4ea8ada525175294c94b48a/rpds_py-2026.6.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:acc992ab27b15f852c76755eb2ab7dce86585ddadba6fa5946e58556088845b4", size = 373783, upload-time = "2026-06-30T07:14:57.736Z" },
+ { url = "https://files.pythonhosted.org/packages/87/fe/e80107ee3639585c9941c17d6a42cd65325022f656c023191fce78c324c8/rpds_py-2026.6.3-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7f88d653e7b3b779d71ae7454e20dcc9b6bae903f33c269db9f2be41bda3f261", size = 378316, upload-time = "2026-06-30T07:14:59.077Z" },
+ { url = "https://files.pythonhosted.org/packages/22/6f/81e3adf81acfb6fa694de2a6e4e7d8863121e3e0799e0a7725e6cf5679c4/rpds_py-2026.6.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e52655eaf81e32593abedaa4bfe33170c8cfedf3365ed9be6e11e07f148f0278", size = 499423, upload-time = "2026-06-30T07:15:00.488Z" },
+ { url = "https://files.pythonhosted.org/packages/2d/9a/41263969df0ce3d9af2a96d5005a288200af1989aed3354bfceb5fc0b21f/rpds_py-2026.6.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dfcc8b909769d19db55c7cc9541eb64b9b774b1057ffffb4f1048070475bb9f9", size = 386077, upload-time = "2026-06-30T07:15:01.911Z" },
+ { url = "https://files.pythonhosted.org/packages/5e/19/7e98f468bd50346faff5b10e5297374b443bfdddacc8e9fbc65984539597/rpds_py-2026.6.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c1255b302953c86a486b81d330d5ee1d5bd937691ce271b6be0ef0e299eaab7", size = 371315, upload-time = "2026-06-30T07:15:03.317Z" },
+ { url = "https://files.pythonhosted.org/packages/99/3c/2b973b4d371906a134b03decfea7f5d9835a2c6d263454392e15b64b5b18/rpds_py-2026.6.3-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:8d2294a31386bfa251d8c8a39472beee17db67d4f1a6eabea665d35c9a4461c3", size = 383502, upload-time = "2026-06-30T07:15:04.627Z" },
+ { url = "https://files.pythonhosted.org/packages/98/2a/12e2799500af0a307bca76b63361c51f9fe479223561489c29eea1f2ee41/rpds_py-2026.6.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f8f23ead891a3b762f35ab3b04623da7056545b48aa60d59957e6789914545da", size = 402673, upload-time = "2026-06-30T07:15:05.856Z" },
+ { url = "https://files.pythonhosted.org/packages/2d/e3/21e5872d165fe08be4f229e3d5ee9d90019c0bf0e5538de60dbd54009450/rpds_py-2026.6.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:421aba32367055614287a4292b6a17f1939c9452299f7a0209c117e990b646d4", size = 549964, upload-time = "2026-06-30T07:15:07.159Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/d0/5ee0fe36844297de8123bee27bc12078c1a7416ad9f1b8a8ca18d6b0c0ac/rpds_py-2026.6.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1e5822dfc2f0d4ab7e745eaa6d85945069329beeccef965af3f3bb26058fcab6", size = 615446, upload-time = "2026-06-30T07:15:08.531Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/80/1ea5873cb683f2fbe5f21b23ea1f6d179ead19f3c5b249b7eb5dca568ef2/rpds_py-2026.6.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:83e35b57523816c8613fd0776b40cd8bb9f596b37ddd2692eb4a6bb5ab2f8c93", size = 576975, upload-time = "2026-06-30T07:15:09.97Z" },
+ { url = "https://files.pythonhosted.org/packages/c9/e1/90ef639217a5ddb15b7f4f61b1c33911fd044ad03c311bafdd2bcab85582/rpds_py-2026.6.3-cp311-cp311-win32.whl", hash = "sha256:de3eceba0b683bcbb1ab93da016d0270df1f9ae7be716b40214c5dafac6ea45a", size = 204453, upload-time = "2026-06-30T07:15:11.324Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/b7/b7a1695d7af36f521fb11e80d6d3adbd744f73b921859bd3c2a2c0dc706f/rpds_py-2026.6.3-cp311-cp311-win_amd64.whl", hash = "sha256:2c54a076ca4d370980ab57bc0e31df57bbe8d41340436a90ef8b1219a3cbb127", size = 223219, upload-time = "2026-06-30T07:15:12.476Z" },
+ { url = "https://files.pythonhosted.org/packages/d7/a2/145afacf796e4506062825941176ad9445c2dcf2b3b6a1f13d3030a15e19/rpds_py-2026.6.3-cp311-cp311-win_arm64.whl", hash = "sha256:168c733a7112e071bb7a66460e667edfcff06c017a3c523f7a8a8e08d0140804", size = 219137, upload-time = "2026-06-30T07:15:13.631Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/be/2e8974163072e7bab7df1a5acd54c4498e75e35d6d18b864d3a9d5dadc92/rpds_py-2026.6.3-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a0811d33247c3d6128a3001d763f2aa056bb3425204335400ac54f89eec3a0d0", size = 343691, upload-time = "2026-06-30T07:15:14.96Z" },
+ { url = "https://files.pythonhosted.org/packages/a4/73/319dfa745dd668efe89309141ded489126461fcecd2b8f3a3cda185129b6/rpds_py-2026.6.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:538949e262e46caa31ac01bdb3c1e8f642622922cacbabbae6a8445d9dc33eaf", size = 338542, upload-time = "2026-06-30T07:15:16.267Z" },
+ { url = "https://files.pythonhosted.org/packages/21/63/4239893be1c4d09b709b1a8f6be4188f0870084ff547f46606b8a75f1b03/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55927d532399c2c646100ff7feb48eaa940ad70f42cd68e1328f3ded9f81ca24", size = 368180, upload-time = "2026-06-30T07:15:17.62Z" },
+ { url = "https://files.pythonhosted.org/packages/1c/ca/9c5de382225234ceb37b1844ebdb140db12b2a278bb9efe2fcd19f6c82ce/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f56f1695bc5c0871cbc33dc0130fcf503aab0c57dcc5a6700a4f49eba4f2652e", size = 375067, upload-time = "2026-06-30T07:15:18.952Z" },
+ { url = "https://files.pythonhosted.org/packages/87/dc/863f69d1bf04ade34b7fe0d59b9fdf6f0135fe2d7cbca74f1d665589559d/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:270b293dae9058fc9fcedab50f13cebf46fb8ed1d1d54e0521a9da5d6b211975", size = 490509, upload-time = "2026-06-30T07:15:20.434Z" },
+ { url = "https://files.pythonhosted.org/packages/ce/ef/eac16a12048b45ec7c7fa94f2be3438a5f26bf9cc8580b18a1cfd609b7f6/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:127565fead0a10943b282957bd5447804ff3160ad79f2ad2635e6d249e380680", size = 382754, upload-time = "2026-06-30T07:15:21.831Z" },
+ { url = "https://files.pythonhosted.org/packages/04/8f/d2f3f532616be4d06c316ef119683e832bd3d41e112bf3a88f4151c95b17/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ecabd69db66de867690f9797f2f8fa27ba501bbc24540cbdbdc649cd15888ba6", size = 366189, upload-time = "2026-06-30T07:15:23.371Z" },
+ { url = "https://files.pythonhosted.org/packages/e3/29/41a7b0e98a4b44cd676ab7598419623373eb43b20be68c084935c1a8cf88/rpds_py-2026.6.3-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:58eadac9cd119677b60e1cf8ac4052f35949d71b8a9e5556efccbe82533cf22a", size = 377750, upload-time = "2026-06-30T07:15:24.659Z" },
+ { url = "https://files.pythonhosted.org/packages/2e/05/ecda0bec46f9a1565090bcdc941d023f6a25aff85fda28f89f8d19878152/rpds_py-2026.6.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7491ee23305ac3eb59e492b6945881f5cd77a6f731061a3f25b77fd40f9e99a4", size = 395576, upload-time = "2026-06-30T07:15:25.987Z" },
+ { url = "https://files.pythonhosted.org/packages/68/a8/6ed52f03ee6cb854ce78785cc9a9a672eb880e83fd7224d471f667d151f1/rpds_py-2026.6.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2c99f7e8ccb3dd6e3e4bfeac657a7b208c9bac8075f4b078c02d7404c34107fa", size = 543807, upload-time = "2026-06-30T07:15:27.356Z" },
+ { url = "https://files.pythonhosted.org/packages/8f/d6/156c0d3eea27ba09b92562ba2364ba124c0a061b199e17eac637cd25a5e2/rpds_py-2026.6.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:62698275682bf121181861295c9181e789030a2d516071f5b8f3c23c170cd0fc", size = 611187, upload-time = "2026-06-30T07:15:28.931Z" },
+ { url = "https://files.pythonhosted.org/packages/f1/31/774212ed989c62f7f310220089f9b0a3fb8f40f5443d1727abd5d9f52bc9/rpds_py-2026.6.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a214c993455f99a89aaeadc9b21241900037adc9d97203e374d75513c5911822", size = 573030, upload-time = "2026-06-30T07:15:30.553Z" },
+ { url = "https://files.pythonhosted.org/packages/c9/50/22f73127a41f1ce4f87fe39aadfb9a126345801c274aa93ae88456249327/rpds_py-2026.6.3-cp312-cp312-win32.whl", hash = "sha256:501f9f04a588d6a09179368c57071301445191767c64e4b52a6aa9871f1ef5ed", size = 202185, upload-time = "2026-06-30T07:15:32.027Z" },
+ { url = "https://files.pythonhosted.org/packages/04/3a/f0ee4d4dde9d3b69dedf1b5f74e7a40017046d55052d173e418c6a94f960/rpds_py-2026.6.3-cp312-cp312-win_amd64.whl", hash = "sha256:2c958bf94822e9290a40aaf2a822d4bc5c88099093e3948ad6c571eca9272e5f", size = 220394, upload-time = "2026-06-30T07:15:33.359Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/83/3382fe37f809b59f02aac04dbc4e765b480b46ee0227ed516e3bdc4d3dfc/rpds_py-2026.6.3-cp312-cp312-win_arm64.whl", hash = "sha256:22bffe6042b9bcb0822bcd1955ec00e245daf17b4344e4ed8e9551b976b63e96", size = 215753, upload-time = "2026-06-30T07:15:34.778Z" },
+ { url = "https://files.pythonhosted.org/packages/a4/9e/b818ee580026ec578138e961027a68820c40afeb1ec8f6819b54fb99e196/rpds_py-2026.6.3-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:3cfe765c1da0072636ca06628261e0ea05688e160d5c8a03e0217c3854037223", size = 343012, upload-time = "2026-06-30T07:15:36.005Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/6b/686d9dc4359a8f163cfbbf89ee0b4e586431de22fe8248edb63a8cf50d49/rpds_py-2026.6.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f4d78253f6996be4901669ad25319f842f740eccf4d58e3c7f3dd39e6dde1d8f", size = 338203, upload-time = "2026-06-30T07:15:37.462Z" },
+ { url = "https://files.pythonhosted.org/packages/9e/9b/069aa329940f8207615e091f5eedbbd40e1e15eac68a0790fd05ccdf796c/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54f45a148e28767bf343d33a684693c70e451c6f4c0e9904709a723fafbdfc1f", size = 367984, upload-time = "2026-06-30T07:15:39.008Z" },
+ { url = "https://files.pythonhosted.org/packages/14/db/34c203e4becff3703e4d3bc121842c00b8689197f398161203a880052f4e/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:842e7b070435622248c7a2c44ae53fa1440e073cc3023bc919fed570884097a7", size = 374815, upload-time = "2026-06-30T07:15:40.253Z" },
+ { url = "https://files.pythonhosted.org/packages/ee/7d/8071067d2cc453d916ad836e828c943f575e8a44612537759002a1e07381/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8020133a74bd81b4572dd8e4be028a6b1ebcd70e6726edc3918008c08bee6ee6", size = 490545, upload-time = "2026-06-30T07:15:41.729Z" },
+ { url = "https://files.pythonhosted.org/packages/a3/42/da06c5aa8f0484ff07f270787434204d9f4535e2f8c3b51ed402267e63c3/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cdc7e35386f3847df728fbcb5e887e2d79c19e2fa1eba9e51b6621d23e3243af", size = 382828, upload-time = "2026-06-30T07:15:43.327Z" },
+ { url = "https://files.pythonhosted.org/packages/57/d7/fe978efc2ae50abe48eb7464668ea99f53c010c60aeebb7b35ad27f23661/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:acac386b453c2516111b50985d60ce46e7fadb5ea71ae7b25f4c946935bf27cf", size = 365678, upload-time = "2026-06-30T07:15:44.992Z" },
+ { url = "https://files.pythonhosted.org/packages/69/9d/1d8922e1990b2a6eb532b6ff53d3e73d2b3bbffc84116c75826bee73dfc6/rpds_py-2026.6.3-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:425560c6fa0415f27261727bb20bd097568485e5eb0c121f1949417d1c516885", size = 377811, upload-time = "2026-06-30T07:15:46.523Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/3d/198dceafb4fb034a6a47347e1b0735d34e0bd4a50be4e898d408ee66cb14/rpds_py-2026.6.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a550fb4950a06dde3beb4721f5ad4b25bf4513784665b0a8522c792e2bd822a4", size = 395382, upload-time = "2026-06-30T07:15:47.955Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/f1/13968e49655d40b6b19d8b9140296bbc6f1d86b3f0f6c346cf9f1adddf4b/rpds_py-2026.6.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4f4bca01b63096f606e095734dd56e74e175f94cfbf24ff3d63281cec61f7bb7", size = 543832, upload-time = "2026-06-30T07:15:49.33Z" },
+ { url = "https://files.pythonhosted.org/packages/ac/ab/289bcb1b90bd3e40a2900c561fa0e2087345ecbb094f0b870f2345142b7c/rpds_py-2026.6.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ccffae9a092a00deb7efd545fe5e2c33c33b88e7c054337e9a74c179347d0b7d", size = 611011, upload-time = "2026-06-30T07:15:50.847Z" },
+ { url = "https://files.pythonhosted.org/packages/1e/16/5043105e679436ccfbc8e5e0dd2d663ed18a8b8113515fd06a5e5d77c83e/rpds_py-2026.6.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1cf01971c4f2c5553b772a542e4aaf191789cd331bc2cd4ff0e6e65ba49e1e97", size = 572431, upload-time = "2026-06-30T07:15:52.394Z" },
+ { url = "https://files.pythonhosted.org/packages/85/ed/adab103321c0a6565d5ae1c2998349bc3ee175b82ccc5ae8fc04cc413075/rpds_py-2026.6.3-cp313-cp313-win32.whl", hash = "sha256:8c3d1e9c15b9d51ca0391e13da1a25a0a4df3c58a37c9dc368e0736cf7f69df0", size = 201710, upload-time = "2026-06-30T07:15:53.894Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/ed/a03b09668e74e5dabbf2e211f6468e1820c0552f7b0500082da31841bf7b/rpds_py-2026.6.3-cp313-cp313-win_amd64.whl", hash = "sha256:9250a9a0a6fd4648b3f868da8d91a4c52b5811a62df58e753d50ae4454a36f80", size = 219454, upload-time = "2026-06-30T07:15:55.25Z" },
+ { url = "https://files.pythonhosted.org/packages/27/17/b8642c12930b71bc2b25831f6708ccf0f75abcd11883932ec9ce54ba3a78/rpds_py-2026.6.3-cp313-cp313-win_arm64.whl", hash = "sha256:900a67df3fd1660b035a4761c4ce73c382ea6b35f90f9863c36c6fd8bf8b09bb", size = 215063, upload-time = "2026-06-30T07:15:56.573Z" },
+ { url = "https://files.pythonhosted.org/packages/b6/36/7fbe9dcdaf857fb3f63c2a2284b62492d95f5e8334e947e5fb6e7f68c9be/rpds_py-2026.6.3-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:931908d9fc855d8f74783377822be318edb6dcb19e47169dc038f9a1bf60b06e", size = 344510, upload-time = "2026-06-30T07:15:57.921Z" },
+ { url = "https://files.pythonhosted.org/packages/ba/54/f785cc3d3f60839ca57a5af4927a9f347b07b2799c373fc20f7949f87c7e/rpds_py-2026.6.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d7469697dce35be237db177d42e2a2ee26e6dcc5fc052078a6fefabd288c6edd", size = 339495, upload-time = "2026-06-30T07:15:59.238Z" },
+ { url = "https://files.pythonhosted.org/packages/63/ef/d4cdaf309e6b095b43597103cf8c0b951d6cca2acce68c474f75ec12e0c7/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bcfbcf66006befb9fd2aeaa9e01feaf881b4dc330a02ba07d2322b1c11be7b5d", size = 369454, upload-time = "2026-06-30T07:16:01.021Z" },
+ { url = "https://files.pythonhosted.org/packages/96/4a/9559a68b7ee15db09d7981212e8c2e219d2a1d6d4faa0391d813c3496a36/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:847927daf4cffbd4e90e42bc890069897101edd015f956cb8721b3473372edda", size = 374583, upload-time = "2026-06-30T07:16:02.287Z" },
+ { url = "https://files.pythonhosted.org/packages/ef/75/8964aa7d2c6e8ac43eba8eb6e6b0fdda1f46d39f2fc3e6aa9f2cb17f485d/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aca6c1ef08a82bfe327cc156da694660f599923e2e6665b6d81c9c2d0ac9ffc8", size = 492919, upload-time = "2026-06-30T07:16:03.723Z" },
+ { url = "https://files.pythonhosted.org/packages/8f/97/6908094ac804115e65aedfd90f1b5fee4eebebd3f6c4cfc5419939267565/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ae50181a047c871561212bb97f7932a2d45fb53e947bd9b57ebad85b529cbc53", size = 383725, upload-time = "2026-06-30T07:16:05.305Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/9c/0d1fdc2e7aba23e290d603bc494e97bd205bae262ce33c6b32a69768ed5e/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc319e5a1de4b6913aac94bf6a2f9e847371e0a140a43dd4991db1a09bc2d504", size = 367255, upload-time = "2026-06-30T07:16:07.086Z" },
+ { url = "https://files.pythonhosted.org/packages/c4/fe/f0209ca4a9ed074bc8acb44dfd0e81c3122e94c9689f5645b7973a866719/rpds_py-2026.6.3-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:e4316bf32babbed84e691e352faf967ce2f0f024174a8643c37c94a1080374fc", size = 379060, upload-time = "2026-06-30T07:16:08.525Z" },
+ { url = "https://files.pythonhosted.org/packages/c6/8d/f1cc54c616b9d8897de8738aac148d20afca93f68187475fe194d09a71b9/rpds_py-2026.6.3-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8c6e5a2f750cc71c3e3b11d71661f21d6f9bc6cebc6564b1466417a1ec03ec77", size = 395960, upload-time = "2026-06-30T07:16:09.989Z" },
+ { url = "https://files.pythonhosted.org/packages/fb/04/aafff00f73aeca2945f734f1d483c64ab8f472d0864ab02377fd8e89c3b2/rpds_py-2026.6.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4470ce197d4090875cf6affbf1f853338387428df97c4fb7b7106317b8214698", size = 545356, upload-time = "2026-06-30T07:16:11.816Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/cc/e229663b9e4ddac5a4acbe9085dd80a71af2a5d356b8b39d6bff233f24b0/rpds_py-2026.6.3-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ea964164cc9afa72d4d9b23cc28dafae93693c0a53e0b42acbff15b22c3f9ddd", size = 612319, upload-time = "2026-06-30T07:16:13.586Z" },
+ { url = "https://files.pythonhosted.org/packages/e3/7a/8a0e6d3e6cd066af108b71b43122c3fe158dd9eb86acac626593a2582eb1/rpds_py-2026.6.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:639c8929aa0afe81be836b04de888460d6bed38b9c54cfc18da8f6bfabf5af5d", size = 573508, upload-time = "2026-06-30T07:16:15.23Z" },
+ { url = "https://files.pythonhosted.org/packages/87/03/2a69ab618a789cf6cf85c86bb844c62d090e700ab1a2aa676b3741b6c516/rpds_py-2026.6.3-cp314-cp314-win32.whl", hash = "sha256:882076c00c0a608b131187055ddc5ae29f2e7eaf870d6168980420d58528a5c8", size = 202504, upload-time = "2026-06-30T07:16:16.893Z" },
+ { url = "https://files.pythonhosted.org/packages/85/62/a3892ba945f4e24c78f352e5de3c7620d8479f73f211406a97263d13c7d2/rpds_py-2026.6.3-cp314-cp314-win_amd64.whl", hash = "sha256:0be972be84cfcaf46c8c6edf690ca0f154ac17babf1f6a955a51579b34ad2dc5", size = 220380, upload-time = "2026-06-30T07:16:18.108Z" },
+ { url = "https://files.pythonhosted.org/packages/3d/e7/c2bd44dc831931815ad11ebb5f430b5a0a4d3caa9de837107876c30c3432/rpds_py-2026.6.3-cp314-cp314-win_arm64.whl", hash = "sha256:2a9c6f195058cb45335e8cc3802745c603d716eb96bc9625950c1aac71c0c703", size = 215976, upload-time = "2026-06-30T07:16:19.654Z" },
+ { url = "https://files.pythonhosted.org/packages/79/9c/fff7b74bce9a091ec9a012a03f9ff5f69364eaf9451060dfc4486da2ffdd/rpds_py-2026.6.3-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:f90938e92afda60266da758ee7d363447f7f0138c9559f9e1811629580582d90", size = 346840, upload-time = "2026-06-30T07:16:21.268Z" },
+ { url = "https://files.pythonhosted.org/packages/e9/44/77bcb1168b33704908295533d27f10eb811e9e3e193e8993dc99572211d3/rpds_py-2026.6.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ec829541c45bca16e61c7ae50c20501f213605beb75d1aba91a6ee37fbbb56a4", size = 340282, upload-time = "2026-06-30T07:16:22.875Z" },
+ { url = "https://files.pythonhosted.org/packages/87/3c/7a9081c7c9e645b39efe19e4ffbeccd80add246327cd9b888aecffd72317/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afd70d95892096cdb26f15a00c45907b17817577aa8d1c76b2dcc2788391f9e9", size = 370403, upload-time = "2026-06-30T07:16:24.415Z" },
+ { url = "https://files.pythonhosted.org/packages/f7/69/af47021eb7dad6ff3396cb001c08f0f3c4d06c20253f75be6421a59fe6b7/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:29dfa0533a5d4c94d4dfa1b694fcb56c9c63aad8330ffdd816fd225d0a7a162f", size = 376055, upload-time = "2026-06-30T07:16:26.111Z" },
+ { url = "https://files.pythonhosted.org/packages/81/fc/a3bcf517084396a6dd258c592567a3c011ba4557f2fde23dceaf26e74f2e/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:af05d726809bff6b141be124d4c7ce998f9c9c7f30edb1f46c07aa103d540b41", size = 494419, upload-time = "2026-06-30T07:16:27.596Z" },
+ { url = "https://files.pythonhosted.org/packages/c9/eb/13d529d1788135425c7bf207f8463458ca5d92e43f3f701365b83e9dffc1/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9826217f048f620d9a712672818bf231442c1b35d96b227a07eabd11b4bb6945", size = 384848, upload-time = "2026-06-30T07:16:29.183Z" },
+ { url = "https://files.pythonhosted.org/packages/8e/f4/b7ac49f30013aba8f7b9566b1dd07e81de95e708c1374b7bacc5b9bc5c9c/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:536bceea4fa4acf7e1c61da2b5786304367c816c8895be71b8f537c480b0ea1f", size = 371369, upload-time = "2026-06-30T07:16:30.912Z" },
+ { url = "https://files.pythonhosted.org/packages/31/86/6260bafa622f788b07ddec0e52d810305c8b9b0b8c27f58a2ab04bf62b4f/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:bc0011654b91cc4fb2ae701bec0a0ba1e552c0714247fa7af6c59e0ccfa3a4e1", size = 379673, upload-time = "2026-06-30T07:16:32.486Z" },
+ { url = "https://files.pythonhosted.org/packages/19/c3/03f1ee79a047b48daeca157c89a18509cde22b6b951d642b9b0af1be660a/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:539d75de9e0d536c84ff18dfeb805398e58227001ce09231a26a08b9aed1ee0e", size = 397500, upload-time = "2026-06-30T07:16:34.471Z" },
+ { url = "https://files.pythonhosted.org/packages/f0/95/8ed0cd8c377dca12aea498f119fe639fc474d1461545c39d2b5872eb1c0f/rpds_py-2026.6.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:166cf54d9f44fc6ceb53c7860258dde44a81406646de79f8ed3234fca3b6e538", size = 545978, upload-time = "2026-06-30T07:16:36.45Z" },
+ { url = "https://files.pythonhosted.org/packages/d3/f2/0eb57f0eaa83f8fc152a7e03de968ab77e1f00732bebc892b190c6eebde7/rpds_py-2026.6.3-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:d34c20167764fbcf927194d532dd7e0c56772f0a5f943fa5ef9e9afbba8fb9db", size = 613350, upload-time = "2026-06-30T07:16:38.213Z" },
+ { url = "https://files.pythonhosted.org/packages/5b/de/e0674bdbc3ef7634989b3f854c3f34bc1f587d36e5bfdc5c378d57034619/rpds_py-2026.6.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ea7bb13b7c9a29791f87a0387ba7d3ad3a6d783d827e4d3f27b40a0ff44495e2", size = 576486, upload-time = "2026-06-30T07:16:39.797Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/f6/21101359743cd136ada781e8210a85769578422ba460672eea0e29739200/rpds_py-2026.6.3-cp314-cp314t-win32.whl", hash = "sha256:6de4744d05bd1aa1be4ed7ea1189e3979196808008113bbbf899a460966b925e", size = 201068, upload-time = "2026-06-30T07:16:41.316Z" },
+ { url = "https://files.pythonhosted.org/packages/a6/b2/9574d4d44f7760c2aa32d92a0a4f41698e33f5b204a0bf5c9758f52c79d5/rpds_py-2026.6.3-cp314-cp314t-win_amd64.whl", hash = "sha256:c7b9a2f8f4d8e90af72571d3d495deebdd7e3c75451f5b41719aee166e940fc2", size = 220600, upload-time = "2026-06-30T07:16:43.091Z" },
+ { url = "https://files.pythonhosted.org/packages/08/ae/f23a2697e6ee6340a578b0f136be6483657bef0c6f9497b752bb5c0964bb/rpds_py-2026.6.3-cp315-cp315-macosx_10_12_x86_64.whl", hash = "sha256:e059c5dde6452b44424bd1834557556c226b57781dee1227af23518459722b13", size = 344726, upload-time = "2026-06-30T07:16:44.5Z" },
+ { url = "https://files.pythonhosted.org/packages/c3/63/e7b3a1a5358dd32c930a1062d8e15b67fd6e8922e81df9e91706d66ee5c8/rpds_py-2026.6.3-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:2f7c26fbc5acd2522b95d4177fe4710ffd8e9b20529e703ffbf8db4d93903f05", size = 339587, upload-time = "2026-06-30T07:16:46.255Z" },
+ { url = "https://files.pythonhosted.org/packages/ec/64/10a85681916ca55fffb91b0a211f84e34297c109243484dd6394660a8a7c/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3086b538543802f84c843911242db20447de00d8752dd0efc936dbcf02218ba", size = 369585, upload-time = "2026-06-30T07:16:48.101Z" },
+ { url = "https://files.pythonhosted.org/packages/76/c2/baf95c7c38823e12ba34407c5f5767a89e5cf2233895e56f608167ae9493/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8f2e5c5ee828d42cb11760761c0af6507927bec42d0ad5458f97c9203b054617", size = 375479, upload-time = "2026-06-30T07:16:49.93Z" },
+ { url = "https://files.pythonhosted.org/packages/6a/94/0aad06c72d65101e11d33528d438cda99a39ce0da99466e156158f2541d3/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ed0c1e5d10cdc7135537988c74a0188da68e2f3c30813ba3744ab1e42e0480f9", size = 492418, upload-time = "2026-06-30T07:16:51.641Z" },
+ { url = "https://files.pythonhosted.org/packages/b5/17/de3f5a479a1f056535d7489819639d8cd591ea6281d700390b43b1abd745/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c2642a7603ec0b16ed77da4555db3b4b472341904873788327c0b0d7b95f1bb", size = 384123, upload-time = "2026-06-30T07:16:53.622Z" },
+ { url = "https://files.pythonhosted.org/packages/46/7d/bf09bd1b145bb2671c03e1e6d1ab8651858d90d8c7dfeadd85a37a934fd8/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8e4320744c1ffdd95a603def63344bfab2d33edeab301c5007e7de9f9f5b3885", size = 367351, upload-time = "2026-06-30T07:16:55.241Z" },
+ { url = "https://files.pythonhosted.org/packages/a3/ea/1bb734f314b8be319149ddee80b18bd41372bdcfbdf88d28131c0cd37719/rpds_py-2026.6.3-cp315-cp315-manylinux_2_31_riscv64.whl", hash = "sha256:a9f4645593036b81bbdb36b9c8e0ea0d1c3fee968c4d59db0344c14087ef143a", size = 378827, upload-time = "2026-06-30T07:16:56.841Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/93/d9611e5b25e26df9a3649813ed66193ace9347a7c7fc4ab7cf70e94851c0/rpds_py-2026.6.3-cp315-cp315-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e55d236be29255554da47abe5c577637db7c24a02b8b46f0ca9524c855801868", size = 395966, upload-time = "2026-06-30T07:16:58.557Z" },
+ { url = "https://files.pythonhosted.org/packages/c3/cb/99d77e16e5534ae1d90629bbe419ba6ee170833a6a85e3aa1cc41726fbbc/rpds_py-2026.6.3-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:24e9c5386e16669b674a69c156c8eeefcb578f3b3397b713b08e6d60f3c7b187", size = 545680, upload-time = "2026-06-30T07:17:00.164Z" },
+ { url = "https://files.pythonhosted.org/packages/59/15/11a29755f790cef7a2f755e8e14f4f0c33f39489e1893a632a2eee59672b/rpds_py-2026.6.3-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:c60924535c75f1566b6eb75b5c31a48a43fef04fa2d0d201acbad8a9969c6107", size = 611853, upload-time = "2026-06-30T07:17:01.962Z" },
+ { url = "https://files.pythonhosted.org/packages/68/86/0c27547e21644da938fb530f7e1a8148dd24d02db07e7a5f2567a17ce710/rpds_py-2026.6.3-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:38a2fea2787428f811719ceb9114cb78964a3138838320c29ac39526c79c16ba", size = 573715, upload-time = "2026-06-30T07:17:03.693Z" },
+ { url = "https://files.pythonhosted.org/packages/29/71/4d8fcf700931815594bce892255bbd973b94efaf0fc1932b0590df18d886/rpds_py-2026.6.3-cp315-cp315-win32.whl", hash = "sha256:d483fe17f01ad64b7bf7cc38fcefff1ca9fb83f8c2b2542b68f97ffe0611b369", size = 202864, upload-time = "2026-06-30T07:17:05.746Z" },
+ { url = "https://files.pythonhosted.org/packages/eb/62/b577562de0edbb55b2be85ce5fd09c33e386b9b13eee09833af4240fd5c4/rpds_py-2026.6.3-cp315-cp315-win_amd64.whl", hash = "sha256:67e3a721ffc5d8d2210d3671872298c4a84e4b8035cfe42ffd7cde35d772b146", size = 220430, upload-time = "2026-06-30T07:17:07.471Z" },
+ { url = "https://files.pythonhosted.org/packages/c8/95/d6d0b2509825141eef60669a5739eec88dbc6a48053d6c92993a5704defe/rpds_py-2026.6.3-cp315-cp315-win_arm64.whl", hash = "sha256:6e84adbcf4bf841aed8116a8264b9f50b4cb3e7bd89b516122e616ac56ca269e", size = 215877, upload-time = "2026-06-30T07:17:09.008Z" },
+ { url = "https://files.pythonhosted.org/packages/b7/bf/f3ea278f0afd615c1d0f19cb69043a41526e2bb600c2b536eb192218eb27/rpds_py-2026.6.3-cp315-cp315t-macosx_10_12_x86_64.whl", hash = "sha256:ae6dd8f10bd17aad820876d24caec9efdafd80a318d16c0a48edb5e136902c6b", size = 346933, upload-time = "2026-06-30T07:17:10.762Z" },
+ { url = "https://files.pythonhosted.org/packages/9d/29/9907bdf1c5346763cf10b7f6852aad86652168c259def904cbe0082c5864/rpds_py-2026.6.3-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:bdbd97738551fca3917c1bd7188bec1920bb520104f28e7e1007f9ceb17b7690", size = 340274, upload-time = "2026-06-30T07:17:12.266Z" },
+ { url = "https://files.pythonhosted.org/packages/6f/2c/8e03767b5778ef25cebf74a7a91a2c3806f8eced4c92cb7406bbe060756d/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b95977e7211527ab0ba576e286d023389fbeeb32a6b7b771665d333c60e5342", size = 370763, upload-time = "2026-06-30T07:17:14.107Z" },
+ { url = "https://files.pythonhosted.org/packages/2e/e1/df2a7e1ba2efd796af26194250b8d42c821b46592311595162af9ef0528d/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d15fde0e6fb0d88a60d221204873743e5d9f0b7d29165e62cd86d0413ad74ba6", size = 376467, upload-time = "2026-06-30T07:17:15.76Z" },
+ { url = "https://files.pythonhosted.org/packages/6b/de/8a0814d1946af29cb068fb259aa8622f856df1d0bab58429448726b537f5/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a136d453475ac0fcbda502ef1e6504bd28d6d904700915d278deeab0d00fe140", size = 496689, upload-time = "2026-06-30T07:17:17.308Z" },
+ { url = "https://files.pythonhosted.org/packages/df/f3/f19e0c852ba13694f5a79f3b719331051573cb5693feacf8a88ffffc3a71/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f826877d462181e5eb1c26a0026b8d0cab05d99844ecb6d8bf3627a2ca0c0442", size = 385340, upload-time = "2026-06-30T07:17:18.928Z" },
+ { url = "https://files.pythonhosted.org/packages/e2/ae/7ec3a9d2d4351f99e37bcb06b6b6f954512646bfdbf9742e1de727865daf/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:79486287de1730dbaff3dbd124d0ca4d2ef7f9d29bf2544f1f93c09b5bcbbd12", size = 372179, upload-time = "2026-06-30T07:17:20.539Z" },
+ { url = "https://files.pythonhosted.org/packages/d3/ac/9cee911dff2aaa9a5a8354f6610bf2e6a616de9197c5fff4f54f82585f1e/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_31_riscv64.whl", hash = "sha256:808345f53cb952433ca2816f1604ff3515608a81784954f38d4452acfe8e61d5", size = 379993, upload-time = "2026-06-30T07:17:22.212Z" },
+ { url = "https://files.pythonhosted.org/packages/83/6b/7c2a07ba88d1e9a936612f7a5d067467ed03d971d5a06f7d309dff044a7e/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1967debc37f64f2c4dc90a7f563aec558b471966e12adcac4e1c4240496b6ebf", size = 398909, upload-time = "2026-06-30T07:17:23.66Z" },
+ { url = "https://files.pythonhosted.org/packages/97/0b/776ffcb66783637b0031f6d58d6fb55913c8b5abf00aeecd46bf933fb477/rpds_py-2026.6.3-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:f0840b5b17057f7fd918b76183a4b5a0635f43e14eb2ce60dce1d4ee4707ea00", size = 546584, upload-time = "2026-06-30T07:17:25.264Z" },
+ { url = "https://files.pythonhosted.org/packages/55/33/ba3bc04d7092bd553c9b2b195624992d2cc4f3de1f380b7b93cbee67bd79/rpds_py-2026.6.3-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:faa679d19a6696fd54259ad321251ad77a13e70e03dd834daa762a44fb6196ef", size = 614357, upload-time = "2026-06-30T07:17:26.888Z" },
+ { url = "https://files.pythonhosted.org/packages/8b/71/14edf065f04630b1a8472f7653cad03f6c478bcf95ea0e6aed55451e33ea/rpds_py-2026.6.3-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:23a439f31ccbeff1574e24889128821d1f7917470e830cf6544dced1c662262a", size = 576533, upload-time = "2026-06-30T07:17:28.546Z" },
+ { url = "https://files.pythonhosted.org/packages/ba/76/65002b08596c389105720a8c0d22298b8dc25a4baf89b2ce431343c8b1de/rpds_py-2026.6.3-cp315-cp315t-win32.whl", hash = "sha256:913ca42ccad3f8cc6e292b587ae8ae49c8c823e5dce51a736252fc7c7cdfa577", size = 201204, upload-time = "2026-06-30T07:17:30.193Z" },
+ { url = "https://files.pythonhosted.org/packages/8c/97/d855d6b3c322d1f27e26f5241c42016b56cf01377ea8ed348285f54652f0/rpds_py-2026.6.3-cp315-cp315t-win_amd64.whl", hash = "sha256:ae3d4fe8c0b9213624fdce7279d70e3b148b682ca20719ebd193a23ebfa47324", size = 220719, upload-time = "2026-06-30T07:17:31.788Z" },
+ { url = "https://files.pythonhosted.org/packages/b4/9c/f0d19ac587fd0e4ab6b72cda355e9c5a6166b01ef7e064e437aef8eb9fef/rpds_py-2026.6.3-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:4cf2d36a2357e4d07bb5a4f98801265327b48256867816cfd2ceb001e9754a8f", size = 349791, upload-time = "2026-06-30T07:17:33.315Z" },
+ { url = "https://files.pythonhosted.org/packages/38/c7/1d49d204c9fd2ee6c537601dc4c1ba921e03363ca576bfab94a00254ac9a/rpds_py-2026.6.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:30c6dc199b24a5e3e81d50da0f00858c5bbdb2617a750395687f4339c5818171", size = 352842, upload-time = "2026-06-30T07:17:34.897Z" },
+ { url = "https://files.pythonhosted.org/packages/ac/e5/c0b5dc93cd0d4c06ce1f438907649514e2ea077bcd911e3154a51e96c38e/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9891e594296ab9dada6551c8e7b387b2721f27a67eecd528412e8906247a7b90", size = 382094, upload-time = "2026-06-30T07:17:36.514Z" },
+ { url = "https://files.pythonhosted.org/packages/0d/54/ec0e907b4ca8d541112db352409bd15f871c9b243e0c92c9b5a46ae96f01/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b5c2dc92304aa48a4a60443b548bb12f12e119d4b72f314015e67b9e1be97fca", size = 388662, upload-time = "2026-06-30T07:17:38.235Z" },
+ { url = "https://files.pythonhosted.org/packages/d3/f4/921c22a4fd0f1c1ac13a3996ffbf0aa67951e2c8ad0d1d9574938a2932e8/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:127e08c0642d880cf32ca47ec2a4a77b901f7e2dd1ad9762adb13955d72ffcc9", size = 504896, upload-time = "2026-06-30T07:17:39.689Z" },
+ { url = "https://files.pythonhosted.org/packages/0b/1b/a114b972cefa1ab1cdb3c7bb177cd3844a12826c507c722d3a73516dbbaf/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8bb68f03f395eb793220b45c097bd4d8c32944393da0fad8b999efac0868fc8c", size = 391545, upload-time = "2026-06-30T07:17:41.336Z" },
+ { url = "https://files.pythonhosted.org/packages/4e/98/af9b3db77d47fcbe6c8c1f36e2c2147ec70292819e99c325f871584a1c11/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a3450b693fde92133e9f51060568a4c31fcca76d5e53bbd611e689ca446517e9", size = 380059, upload-time = "2026-06-30T07:17:42.857Z" },
+ { url = "https://files.pythonhosted.org/packages/c9/ba/0efd8668b97c1d26a61566386c636a7a7a09829e474fdf807caa15a2c844/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:5e8d07bddee435a2ff6f1920e18feff28d0bc4533e42f4bf6927fbd073312c41", size = 393235, upload-time = "2026-06-30T07:17:44.637Z" },
+ { url = "https://files.pythonhosted.org/packages/62/90/8c139ee9690f73b0829f32647de6f40d826f8f443af6fa72644f96351aac/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3a83ae6c67b7676b9878378547ca8e93ed77a580037bcbcd1d32f739e1e6089c", size = 413008, upload-time = "2026-06-30T07:17:46.225Z" },
+ { url = "https://files.pythonhosted.org/packages/9c/97/0043896fdd7828ce09a1d9a8b06433714d0960fc4ff3fc4aa72b666b764e/rpds_py-2026.6.3-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:2bfd04c19ddbd6640de0b51894d764bd2758854d5b75bd102d2ef10cb9c293a9", size = 558118, upload-time = "2026-06-30T07:17:47.759Z" },
+ { url = "https://files.pythonhosted.org/packages/f6/40/02355f0e134f783a8f9814c4680a1bd311d37671577a5964ea838573ff37/rpds_py-2026.6.3-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:ca6546b66be9dc4738b1b043d5ebd5488c66c578c5ff0fd0e8065313fe3afb76", size = 623138, upload-time = "2026-06-30T07:17:49.355Z" },
+ { url = "https://files.pythonhosted.org/packages/10/85/48f0abdcef5cce4e034c7a5b0ceeceba0b01bf0d942824f4bb720afe2dec/rpds_py-2026.6.3-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:8e65860d238379ed982fd9ba690579b5e95af2f4840f99c772816dbe573cb826", size = 586486, upload-time = "2026-06-30T07:17:51.141Z" },
+]
+
+[[package]]
+name = "typing-extensions"
+version = "4.16.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size = 113555, upload-time = "2026-07-02T08:40:05.92Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" },
+]