From 45651887325ad00d07266608b5afb8824afb98d3 Mon Sep 17 00:00:00 2001 From: Baruch Oxman Date: Mon, 10 Aug 2026 10:20:01 +0300 Subject: [PATCH 1/2] Rebrand the Honeydew converter to Ossie The Honeydew converter still carried the project's former OSI name in its distribution name, package, console script and docs, unlike every other Python converter which uses the apache-ossie- / ossie_ / ossie- convention. Bring it in line: honeydew-osi -> apache-ossie-honeydew (distribution) honeydew_osi -> ossie_honeydew (import package) honeydew-osi -> ossie-honeydew (console script) Prose in the README, module docstrings, comments and CLI help now says Ossie instead of OSI, matching the snowflake and orionbelt converters after the same rebrand. The subcommand names osi-to-honeydew / honeydew-to-osi are unchanged, as are the internal identifiers (SUPPORTED_OSI_VERSION, convert_osi_to_*) and the "osi" Honeydew metadata section name, which is a persisted round-trip key rather than branding. Also adds the final newline .editorconfig requires to the two moved Python files. Signed-off-by: Baruch Oxman --- converters/honeydew/README.md | 38 ++++----- converters/honeydew/pyproject.toml | 8 +- .../__init__.py | 2 +- .../converter.py | 82 +++++++++---------- ...er.py => test_ossie_honeydew_converter.py} | 30 +++---- converters/honeydew/uv.lock | 20 ++--- 6 files changed, 90 insertions(+), 90 deletions(-) rename converters/honeydew/src/{honeydew_osi => ossie_honeydew}/__init__.py (97%) rename converters/honeydew/src/{honeydew_osi => ossie_honeydew}/converter.py (93%) rename converters/honeydew/tests/{test_honeydew_osi_converter.py => test_ossie_honeydew_converter.py} (98%) diff --git a/converters/honeydew/README.md b/converters/honeydew/README.md index df0b35f7..b1b179e0 100644 --- a/converters/honeydew/README.md +++ b/converters/honeydew/README.md @@ -17,20 +17,20 @@ under the License. --> -# OSI ↔ Honeydew Converter +# Apache Ossie ↔ Honeydew Converter -Bidirectional converter between [OSI](../../core-spec/spec.md) semantic models and [Honeydew](https://honeydew.ai/docs) workspace YAML. +Bidirectional converter between [Apache Ossie](../../core-spec/spec.md) semantic models and [Honeydew](https://honeydew.ai/docs) workspace YAML. ## Overview | Direction | Input | Output | |-----------|-------|--------| -| `osi-to-honeydew` | Single OSI YAML file | Honeydew workspace directory | -| `honeydew-to-osi` | Honeydew workspace directory | Single OSI YAML file | +| `osi-to-honeydew` | Single Ossie YAML file | Honeydew workspace directory | +| `honeydew-to-osi` | Honeydew workspace directory | Single Ossie YAML file | -### OSI → Honeydew mapping +### Ossie → Honeydew mapping -| OSI concept | Honeydew concept | +| Ossie concept | Honeydew concept | |-------------|-----------------| | `semantic_model.name` | `workspace.yml name` | | `dataset` | Entity + dataset files under `schema//` | @@ -42,9 +42,9 @@ Bidirectional converter between [OSI](../../core-spec/spec.md) semantic models a | `relationship` (from → to) | `entity.relations` on the "from" entity (`rel_type: many-to-one`) | | `metric` | `metric` YAML (assigned to entity by expression parse) | -### Honeydew → OSI mapping +### Honeydew → Ossie mapping -| Honeydew concept | OSI concept | +| Honeydew concept | Ossie concept | |-----------------|-------------| | `workspace.name` | `semantic_model.name` | | Entity + primary dataset | `dataset` | @@ -69,11 +69,11 @@ uv sync ## Usage ```bash -# OSI YAML → Honeydew workspace directory -uv run honeydew-osi osi-to-honeydew -i input.yaml -o output_dir/ +# Ossie YAML → Honeydew workspace directory +uv run ossie-honeydew osi-to-honeydew -i input.yaml -o output_dir/ -# Honeydew workspace directory → OSI YAML -uv run honeydew-osi honeydew-to-osi -i workspace_dir/ -o output.yaml +# Honeydew workspace directory → Ossie YAML +uv run ossie-honeydew honeydew-to-osi -i workspace_dir/ -o output.yaml ``` ## Tests @@ -84,10 +84,10 @@ uv run pytest ## Limitations -- **One source dataset per entity**: Honeydew entities can have multiple source dataset files; the converter always generates exactly one, because an OSI `dataset` block describes a single table or SQL query. -- **Datatype inference**: OSI fields have no explicit datatype; the converter infers Honeydew datatypes from the `dimension.is_time` flag (`timestamp`) and the presence/absence of the `dimension` key (`string` vs `number`). -- **Honeydew SQL expressions**: Calculated attributes and metrics use Honeydew's `entity.attribute` reference syntax. These are exported as `ANSI_SQL` dialect expressions in OSI; they remain valid for round-tripping but may not run on other databases without adaptation. -- **Perspectives and domains**: Not converted (no OSI equivalent). -- **Connection expressions** (`connection_expr`): Preserved in `HONEYDEW` custom extensions on the OSI relationship and restored on the return trip. -- **`ai_context`**: OSI `ai_context` fields (synonyms, instructions) are stored in Honeydew `metadata` for round-trip recovery. Instructions are also merged into `description` for human readability. -- **`unique_keys`**: A Honeydew entity's `keys` uniquely identify its rows — Honeydew enforces this and validates that relations join to those keys — so they are emitted as the OSI dataset's `primary_key` *and* a `unique_keys` entry. This surfaces the join-target cardinality for OSI consumers (e.g. a many-to-one relation's `to_columns` are always covered by the target's `unique_keys`). A unique key identical to the primary key is not stored back in Honeydew `metadata` on the return trip, so `Honeydew → OSI → Honeydew` stays clean; `OSI → Honeydew → OSI` normalizes by surfacing the primary key as a unique key. +- **One source dataset per entity**: Honeydew entities can have multiple source dataset files; the converter always generates exactly one, because an Ossie `dataset` block describes a single table or SQL query. +- **Datatype inference**: Ossie fields have no explicit datatype; the converter infers Honeydew datatypes from the `dimension.is_time` flag (`timestamp`) and the presence/absence of the `dimension` key (`string` vs `number`). +- **Honeydew SQL expressions**: Calculated attributes and metrics use Honeydew's `entity.attribute` reference syntax. These are exported as `ANSI_SQL` dialect expressions in Ossie; they remain valid for round-tripping but may not run on other databases without adaptation. +- **Perspectives and domains**: Not converted (no Ossie equivalent). +- **Connection expressions** (`connection_expr`): Preserved in `HONEYDEW` custom extensions on the Ossie relationship and restored on the return trip. +- **`ai_context`**: Ossie `ai_context` fields (synonyms, instructions) are stored in Honeydew `metadata` for round-trip recovery. Instructions are also merged into `description` for human readability. +- **`unique_keys`**: A Honeydew entity's `keys` uniquely identify its rows — Honeydew enforces this and validates that relations join to those keys — so they are emitted as the Ossie dataset's `primary_key` *and* a `unique_keys` entry. This surfaces the join-target cardinality for Ossie consumers (e.g. a many-to-one relation's `to_columns` are always covered by the target's `unique_keys`). A unique key identical to the primary key is not stored back in Honeydew `metadata` on the return trip, so `Honeydew → Ossie → Honeydew` stays clean; `Ossie → Honeydew → Ossie` normalizes by surfacing the primary key as a unique key. diff --git a/converters/honeydew/pyproject.toml b/converters/honeydew/pyproject.toml index 24d085f9..7585ec76 100644 --- a/converters/honeydew/pyproject.toml +++ b/converters/honeydew/pyproject.toml @@ -26,9 +26,9 @@ dev = [ ] [project] -name = "honeydew-osi" +name = "apache-ossie-honeydew" version = "0.2.0.dev0" -description = "Bidirectional converter between Honeydew workspace YAML and OSI semantic model" +description = "Honeydew workspace YAML <> Apache Ossie converter" authors = [{ name = "Apache Software Foundation", email = "dev@ossie.apache.org" }] requires-python = ">=3.12" readme = "README.md" @@ -43,14 +43,14 @@ dependencies = [ ] [project.scripts] -honeydew-osi = "honeydew_osi.converter:main" +ossie-honeydew = "ossie_honeydew.converter:main" [project.urls] homepage = "https://ossie.apache.org/" repository = "https://github.com/apache/ossie/" [tool.hatch.build.targets.wheel] -packages = ["src/honeydew_osi"] +packages = ["src/ossie_honeydew"] [tool.pytest.ini_options] testpaths = ["tests"] diff --git a/converters/honeydew/src/honeydew_osi/__init__.py b/converters/honeydew/src/ossie_honeydew/__init__.py similarity index 97% rename from converters/honeydew/src/honeydew_osi/__init__.py rename to converters/honeydew/src/ossie_honeydew/__init__.py index d216be4d..13a83393 100644 --- a/converters/honeydew/src/honeydew_osi/__init__.py +++ b/converters/honeydew/src/ossie_honeydew/__init__.py @@ -13,4 +13,4 @@ # "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. \ No newline at end of file +# under the License. diff --git a/converters/honeydew/src/honeydew_osi/converter.py b/converters/honeydew/src/ossie_honeydew/converter.py similarity index 93% rename from converters/honeydew/src/honeydew_osi/converter.py rename to converters/honeydew/src/ossie_honeydew/converter.py index e3f78f4f..1959b74e 100644 --- a/converters/honeydew/src/honeydew_osi/converter.py +++ b/converters/honeydew/src/ossie_honeydew/converter.py @@ -16,16 +16,16 @@ # under the License. """ -Bidirectional converter between OSI and Honeydew semantic model formats. +Bidirectional converter between Ossie and Honeydew semantic model formats. -OSI → Honeydew: Converts a single OSI YAML file into a Honeydew workspace +Ossie → Honeydew: Converts a single Ossie YAML file into a Honeydew workspace directory (multiple YAML files per entity). -Honeydew → OSI: Reads a Honeydew workspace directory and produces an OSI YAML. +Honeydew → Ossie: Reads a Honeydew workspace directory and produces an Ossie YAML. Usage: - python converter.py osi-to-honeydew -i input.yaml -o output_dir/ - python converter.py honeydew-to-osi -i workspace_dir/ -o output.yaml + ossie-honeydew osi-to-honeydew -i input.yaml -o output_dir/ + ossie-honeydew honeydew-to-osi -i workspace_dir/ -o output.yaml """ import argparse @@ -45,16 +45,16 @@ class HoneydewConversionError(Exception): - """Raised when conversion between OSI and Honeydew fails.""" + """Raised when conversion between Ossie and Honeydew fails.""" # ───────────────────────────────────────────────────────────────────────────── -# OSI → Honeydew +# Ossie → Honeydew # ───────────────────────────────────────────────────────────────────────────── def convert_osi_to_honeydew(osi_yaml_str: str) -> dict[str, str]: - """Convert an OSI YAML string to a Honeydew workspace file tree. + """Convert an Ossie YAML string to a Honeydew workspace file tree. Returns a dict mapping relative file paths to their YAML content strings. The caller writes these to disk under the desired output directory. @@ -76,7 +76,7 @@ def convert_osi_to_honeydew(osi_yaml_str: str) -> dict[str, str]: ``"osi"`` so they can be recovered on the return trip. Args: - osi_yaml_str: OSI YAML document as a string. + osi_yaml_str: Ossie YAML document as a string. Returns: Dict of {relative_path: yaml_content}. @@ -86,12 +86,12 @@ def convert_osi_to_honeydew(osi_yaml_str: str) -> dict[str, str]: """ root = yaml.safe_load(osi_yaml_str) if not isinstance(root, dict): - raise HoneydewConversionError("Invalid OSI YAML: expected a mapping at the root") + raise HoneydewConversionError("Invalid Ossie YAML: expected a mapping at the root") version_str = str(root.get("version", "")) if version_str != SUPPORTED_OSI_VERSION: raise HoneydewConversionError( - f"Unsupported OSI version '{version_str}'. Supported: {SUPPORTED_OSI_VERSION}" + f"Unsupported Ossie version '{version_str}'. Supported: {SUPPORTED_OSI_VERSION}" ) semantic_models = root.get("semantic_model") @@ -100,7 +100,7 @@ def convert_osi_to_honeydew(osi_yaml_str: str) -> dict[str, str]: if len(semantic_models) > 1: warnings.warn( - f"OSI YAML contains {len(semantic_models)} semantic models; " + f"Ossie YAML contains {len(semantic_models)} semantic models; " "only the first will be converted" ) @@ -138,14 +138,14 @@ def _model_to_files(sm: dict[str, Any], *, extra_vendors: list[str] | None = Non entity_names = [ds["name"] for ds in datasets if ds.get("name")] - # Group OSI relationships by from-entity + # Group Ossie relationships by from-entity rel_by_entity: dict[str, list[dict[str, Any]]] = {} for rel in relationships: from_ds = rel.get("from") if from_ds: rel_by_entity.setdefault(from_ds, []).append(rel) - # Assign OSI metrics to entities (honours HONEYDEW entity hint for round-trips) + # Assign Ossie metrics to entities (honours HONEYDEW entity hint for round-trips) metric_by_entity = _assign_metrics_to_entities(metrics, entity_names) for ds in datasets: @@ -167,7 +167,7 @@ def _fields_to_honeydew( fields: list[dict[str, Any]], entity_name: str, ) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]: - """Classify OSI fields into Honeydew dataset attributes and calculated attributes.""" + """Classify Ossie fields into Honeydew dataset attributes and calculated attributes.""" dataset_attrs: list[dict[str, Any]] = [] calc_attrs: list[dict[str, Any]] = [] @@ -194,7 +194,7 @@ def _fields_to_honeydew( instr = field_ai_ctx["instructions"] effective_desc = f"{field_desc}\n{instr}" if field_desc else instr - # Build labels: OSI label + ai_context synonyms + # Build labels: Ossie label + ai_context synonyms labels: list[str] = [] if field_label: labels.append(field_label) @@ -258,8 +258,8 @@ def _dataset_to_files( primary_key = ds.get("primary_key") or [] unique_keys = ds.get("unique_keys") # A unique key identical to the primary key is re-derived from the entity's - # keys on the Honeydew → OSI path, so don't persist it as metadata — that - # would inject a redundant metadata block on a Honeydew → OSI → Honeydew round-trip. + # keys on the Honeydew → Ossie path, so don't persist it as metadata — that + # would inject a redundant metadata block on a Honeydew → Ossie → Honeydew round-trip. if unique_keys: pk_tuple = tuple(primary_key) unique_keys = [uk for uk in unique_keys if tuple(uk) != pk_tuple] or None @@ -291,7 +291,7 @@ def _dataset_to_files( honeydew_relations.append(hr) entity_dict["relations"] = honeydew_relations - # Preserve OSI fields that have no Honeydew native equivalent + # Preserve Ossie fields that have no Honeydew native equivalent entity_meta = _build_osi_metadata( ai_context=ai_context, unique_keys=unique_keys, @@ -467,7 +467,7 @@ def _assign_metrics_to_entities( metrics: list[dict[str, Any]], entity_names: list[str], ) -> dict[str, list[dict[str, Any]]]: - """Assign each OSI metric to the most appropriate Honeydew entity. + """Assign each Ossie metric to the most appropriate Honeydew entity. Priority: 1. HONEYDEW ``custom_extension`` entity hint (preserves round-trip placement) @@ -480,7 +480,7 @@ def _assign_metrics_to_entities( for metric in metrics: mname = metric.get("name", "") - # Priority 1: HONEYDEW entity hint (set during Honeydew → OSI) + # Priority 1: HONEYDEW entity hint (set during Honeydew → Ossie) hinted = _get_honeydew_extension(metric).get("entity") if hinted and hinted in entity_set: result.setdefault(hinted, []).append(metric) @@ -524,15 +524,15 @@ def _find_entity_in_expression(expr: str, entity_names: set[str]) -> str | None: # ───────────────────────────────────────────────────────────────────────────── -# Honeydew → OSI +# Honeydew → Ossie # ───────────────────────────────────────────────────────────────────────────── def convert_honeydew_to_osi(workspace_dir: str) -> str: - """Convert a Honeydew workspace directory to an OSI YAML string. + """Convert a Honeydew workspace directory to an Ossie YAML string. Reads workspace.yml and all entity subdirectories under schema/. Honeydew - fields with no OSI equivalent (``owner``, ``display_name``, ``hidden``, + fields with no Ossie equivalent (``owner``, ``display_name``, ``hidden``, ``format_string``, ``timegrain``, attribute ``labels``) are preserved in a HONEYDEW ``custom_extension`` so they survive a round-trip back to Honeydew. @@ -540,7 +540,7 @@ def convert_honeydew_to_osi(workspace_dir: str) -> str: workspace_dir: Path to the Honeydew workspace root. Returns: - OSI YAML document string. + Ossie YAML document string. Raises: HoneydewConversionError: On missing workspace.yml. @@ -690,7 +690,7 @@ def _entity_to_osi_dataset(entity_data: dict[str, Any]) -> dict[str, Any]: if keys: ds["primary_key"] = list(keys) - # Restore OSI-only fields preserved in Honeydew metadata + # Restore Ossie-only fields preserved in Honeydew metadata osi_meta = entity_data.get("osi_meta") or {} if osi_meta.get("ai_context"): ds["ai_context"] = osi_meta["ai_context"] @@ -698,7 +698,7 @@ def _entity_to_osi_dataset(entity_data: dict[str, Any]) -> dict[str, Any]: # A Honeydew entity's keys uniquely identify its rows — Honeydew enforces this # and validates that relationships join to those keys — so surface them as a # unique key as well as the primary key. Union with any unique keys preserved - # from an OSI source, de-duplicated. + # from an Ossie source, de-duplicated. unique_keys = [list(uk) for uk in (osi_meta.get("unique_keys") or [])] if keys and tuple(keys) not in {tuple(uk) for uk in unique_keys}: unique_keys.append(list(keys)) @@ -740,7 +740,7 @@ def _entity_to_osi_dataset(entity_data: dict[str, Any]) -> dict[str, Any]: # Restore ai_context (structured form takes priority) # Only synthesise synonyms from labels when they are native Honeydew labels - # (i.e. osi_meta has no 'label' key, meaning the label didn't come from OSI) + # (i.e. osi_meta has no 'label' key, meaning the label didn't come from Ossie) if attr_osi_meta.get("ai_context"): field["ai_context"] = attr_osi_meta["ai_context"] elif attr_labels and "label" not in attr_osi_meta: @@ -813,7 +813,7 @@ def _entity_to_osi_dataset(entity_data: dict[str, Any]) -> dict[str, Any]: calc_honeydew_extra["datatype"] = datatype all_calc_ext = list(calc_osi_meta.get("custom_extensions") or []) - # Always mark as calculated_attribute so OSI → Honeydew routes it correctly + # Always mark as calculated_attribute so Ossie → Honeydew routes it correctly all_calc_ext.append({ "vendor_name": HONEYDEW_VENDOR, "data": json.dumps(dict({"type": "calculated_attribute", "entity": entity_name}, **calc_honeydew_extra)), @@ -834,7 +834,7 @@ def _honeydew_datatype_to_osi_dimension(datatype: str) -> dict[str, Any] | None: return {"is_time": True} if dt in ("bool", "string"): return {"is_time": False} - return None # number / float → OSI fact (no dimension key) + return None # number / float → Ossie fact (no dimension key) def _honeydew_relation_to_osi( @@ -920,7 +920,7 @@ def _honeydew_metric_to_osi(metric: dict[str, Any], entity_name: str) -> dict[st # ───────────────────────────────────────────────────────────────────────────── -# OSI metadata helpers — store/restore OSI fields in Honeydew metadata sections +# Ossie metadata helpers — store/restore Ossie fields in Honeydew metadata sections # ───────────────────────────────────────────────────────────────────────────── @@ -932,7 +932,7 @@ def _build_osi_metadata( custom_extensions: list | None = None, extra_vendors: list[str] | None = None, ) -> dict[str, Any] | None: - """Build a Honeydew metadata entry that stores OSI-only fields for round-tripping.""" + """Build a Honeydew metadata entry that stores Ossie-only fields for round-tripping.""" items: list[dict[str, Any]] = [] if ai_context is not None: @@ -953,7 +953,7 @@ def _build_osi_metadata( def _read_osi_metadata(obj: dict[str, Any]) -> dict[str, Any]: - """Read OSI-preserved fields from a Honeydew object's 'osi' metadata section.""" + """Read Ossie-preserved fields from a Honeydew object's 'osi' metadata section.""" for section in (obj.get("metadata") or []): if (section.get("name") or "") != _OSI_METADATA_SECTION: continue @@ -972,13 +972,13 @@ def _read_osi_metadata(obj: dict[str, Any]) -> dict[str, Any]: try: result[key] = json.loads(raw) except (json.JSONDecodeError, TypeError): - warnings.warn(f"Could not parse OSI metadata field '{key}': {raw!r}") + warnings.warn(f"Could not parse Ossie metadata field '{key}': {raw!r}") return result return {} def _get_honeydew_extension(obj: dict[str, Any]) -> dict[str, Any]: - """Extract the HONEYDEW custom_extension data from an OSI object.""" + """Extract the HONEYDEW custom_extension data from an Ossie object.""" for ext in (obj.get("custom_extensions") or []): if ext.get("vendor_name") == HONEYDEW_VENDOR: try: @@ -1018,17 +1018,17 @@ def _check_safe_path(output_abs: str, rel_path: str) -> bool: def main() -> None: parser = argparse.ArgumentParser( - description="Bidirectional OSI ↔ Honeydew semantic model converter" + description="Bidirectional Ossie ↔ Honeydew semantic model converter" ) sub = parser.add_subparsers(dest="command", required=True) - p1 = sub.add_parser("osi-to-honeydew", help="Convert OSI YAML → Honeydew workspace") - p1.add_argument("-i", "--input", required=True, help="OSI YAML input file") + p1 = sub.add_parser("osi-to-honeydew", help="Convert Ossie YAML → Honeydew workspace") + p1.add_argument("-i", "--input", required=True, help="Ossie YAML input file") p1.add_argument("-o", "--output", required=True, help="Output directory for Honeydew workspace") - p2 = sub.add_parser("honeydew-to-osi", help="Convert Honeydew workspace → OSI YAML") + p2 = sub.add_parser("honeydew-to-osi", help="Convert Honeydew workspace → Ossie YAML") p2.add_argument("-i", "--input", required=True, help="Honeydew workspace directory") - p2.add_argument("-o", "--output", required=True, help="OSI YAML output file") + p2.add_argument("-o", "--output", required=True, help="Ossie YAML output file") args = parser.parse_args() @@ -1065,4 +1065,4 @@ def main() -> None: if __name__ == "__main__": - main() \ No newline at end of file + main() diff --git a/converters/honeydew/tests/test_honeydew_osi_converter.py b/converters/honeydew/tests/test_ossie_honeydew_converter.py similarity index 98% rename from converters/honeydew/tests/test_honeydew_osi_converter.py rename to converters/honeydew/tests/test_ossie_honeydew_converter.py index c3aaa75c..9eaa6733 100644 --- a/converters/honeydew/tests/test_honeydew_osi_converter.py +++ b/converters/honeydew/tests/test_ossie_honeydew_converter.py @@ -15,7 +15,7 @@ # specific language governing permissions and limitations # under the License. -"""Tests for the bidirectional OSI ↔ Honeydew converter.""" +"""Tests for the bidirectional Ossie ↔ Honeydew converter.""" import json import os @@ -26,7 +26,7 @@ import pytest import yaml -from honeydew_osi.converter import ( +from ossie_honeydew.converter import ( HoneydewConversionError, _assign_metrics_to_entities, _build_osi_metadata, @@ -134,7 +134,7 @@ def _write_workspace(tmp_dir, workspace_name, entities): def _osi_roundtrip(model_dict, tmp_path): - """OSI → Honeydew → OSI; returns the semantic model dict.""" + """Ossie → Honeydew → Ossie; returns the semantic model dict.""" files = convert_osi_to_honeydew(_osi(model_dict)) for rel_path, content in files.items(): p = tmp_path / rel_path @@ -144,7 +144,7 @@ def _osi_roundtrip(model_dict, tmp_path): def _honeydew_roundtrip(entities, tmp_path): - """Honeydew → OSI → Honeydew; returns Path to the output workspace directory.""" + """Honeydew → Ossie → Honeydew; returns Path to the output workspace directory.""" _write_workspace(str(tmp_path), "ws", entities) osi_yaml = convert_honeydew_to_osi(str(tmp_path)) files = convert_osi_to_honeydew(osi_yaml) @@ -244,7 +244,7 @@ def test_pick_ansi_expression_non_dict_warns(): # ───────────────────────────────────────────────────────────────────────────── -# OSI metadata helpers +# Ossie metadata helpers # ───────────────────────────────────────────────────────────────────────────── def test_build_and_read_ai_context_string(): @@ -339,7 +339,7 @@ def test_check_safe_path(rel_path, expected): # ───────────────────────────────────────────────────────────────────────────── -# OSI → Honeydew: file content +# Ossie → Honeydew: file content # ───────────────────────────────────────────────────────────────────────────── _REL_MODEL = { @@ -604,7 +604,7 @@ def test_osi_to_honeydew_multiple_models_warns(): # ───────────────────────────────────────────────────────────────────────────── -# Honeydew → OSI: full document +# Honeydew → Ossie: full document # ───────────────────────────────────────────────────────────────────────────── def _hd_root(sm): @@ -763,7 +763,7 @@ def _ansi(expr): }]}), id="metric", ), - # ── calculated attribute → OSI field with HONEYDEW extension ───────────── + # ── calculated attribute → Ossie field with HONEYDEW extension ───────────── pytest.param( "ws", [{"name": "orders", "keys": ["id"], "key_dataset": "orders", "sql": "db.s.orders", @@ -833,7 +833,7 @@ def test_honeydew_to_osi_relation_target_columns_are_unique_keys(tmp_path): # Honeydew validates that a many-to-one relation joins to the target entity's # keys, so the relation's to_columns are always covered by the target dataset's # unique_keys (derived from those keys). This preserves the cardinality metadata - # for OSI consumers (e.g. Snowflake) without inspecting the relation itself. + # for Ossie consumers (e.g. Snowflake) without inspecting the relation itself. _write_workspace(str(tmp_path), "ws", [ {"name": "orders", "keys": ["order_id"], "key_dataset": "orders", "sql": "db.s.orders", "relations": [{"target_entity": "customers", "rel_type": "many-to-one", @@ -850,7 +850,7 @@ def test_honeydew_to_osi_relation_target_columns_are_unique_keys(tmp_path): # ───────────────────────────────────────────────────────────────────────────── -# OSI → Honeydew → OSI round-trip: full semantic model +# Ossie → Honeydew → Ossie round-trip: full semantic model # ───────────────────────────────────────────────────────────────────────────── @pytest.mark.parametrize("model,expected_sm", [ @@ -862,7 +862,7 @@ def test_honeydew_to_osi_relation_target_columns_are_unique_keys(tmp_path): pytest.param( {"name": "m", "datasets": [{"name": "orders", "source": "db.s.orders", "primary_key": ["order_id"], "fields": []}]}, - # OSI → Honeydew → OSI normalizes: the primary key surfaces as a unique key, + # Ossie → Honeydew → Ossie normalizes: the primary key surfaces as a unique key, # because Honeydew expresses uniqueness through entity keys. {"name": "m", "datasets": [{"name": "orders", "source": "db.s.orders", "primary_key": ["order_id"], "unique_keys": [["order_id"]]}]}, @@ -997,7 +997,7 @@ def test_osi_roundtrip_tpcds_example(tmp_path): # ───────────────────────────────────────────────────────────────────────────── -# Honeydew → OSI → Honeydew round-trip: full file content +# Honeydew → Ossie → Honeydew round-trip: full file content # ───────────────────────────────────────────────────────────────────────────── @pytest.mark.parametrize("entities,path,expected", [ @@ -1444,7 +1444,7 @@ def test_main_osi_to_honeydew(tmp_path): })) output_dir = tmp_path / "out" result = subprocess.run( - [sys.executable, "-m", "honeydew_osi.converter", + [sys.executable, "-m", "ossie_honeydew.converter", "osi-to-honeydew", "-i", str(input_file), "-o", str(output_dir)], capture_output=True, text=True, ) @@ -1462,7 +1462,7 @@ def test_main_honeydew_to_osi(tmp_path): }]) output_file = tmp_path / "output.yaml" result = subprocess.run( - [sys.executable, "-m", "honeydew_osi.converter", + [sys.executable, "-m", "ossie_honeydew.converter", "honeydew-to-osi", "-i", str(tmp_path), "-o", str(output_file)], capture_output=True, text=True, ) @@ -1487,7 +1487,7 @@ def test_main_path_traversal_rejected(tmp_path): ) output_dir = tmp_path / "out" result = subprocess.run( - [sys.executable, "-m", "honeydew_osi.converter", + [sys.executable, "-m", "ossie_honeydew.converter", "osi-to-honeydew", "-i", str(input_file), "-o", str(output_dir)], capture_output=True, text=True, ) diff --git a/converters/honeydew/uv.lock b/converters/honeydew/uv.lock index 1220f531..19f70065 100644 --- a/converters/honeydew/uv.lock +++ b/converters/honeydew/uv.lock @@ -3,16 +3,7 @@ revision = 3 requires-python = ">=3.12" [[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 = "honeydew-osi" +name = "apache-ossie-honeydew" version = "0.2.0.dev0" source = { editable = "." } dependencies = [ @@ -30,6 +21,15 @@ requires-dist = [{ name = "pyyaml", specifier = ">=6.0" }] [package.metadata.requires-dev] dev = [{ name = "pytest", specifier = ">=8.0" }] +[[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" From 6b9e12a17c6aed600337d42384ffe775948c660f Mon Sep 17 00:00:00 2001 From: Baruch Oxman Date: Mon, 10 Aug 2026 12:44:09 +0300 Subject: [PATCH 2/2] Rename the remaining OSI identifiers and link Honeydew's Ossie docs Follow-up to the packaging rebrand, now that the OSI-era names can go: osi-to-honeydew / honeydew-to-osi -> ossie-to-honeydew / honeydew-to-ossie SUPPORTED_OSI_VERSION -> SUPPORTED_OSSIE_VERSION convert_osi_to_honeydew -> convert_ossie_to_honeydew convert_honeydew_to_osi -> convert_honeydew_to_ossie _build/_read_osi_metadata, _entity_to_osi_dataset, _parse_osi_source and the remaining _osi_* helpers and osi_* locals The Honeydew metadata section the converter writes to preserve Ossie-only fields is now named "ossie" rather than "osi". Because that name is a persisted key, `_read_ossie_metadata` still accepts the legacy "osi" section: without it, exporting a workspace produced by an earlier version would silently drop every preserved ai_context, label, unique_keys and custom_extensions entry. Covered by test_read_legacy_osi_metadata_section. Also points the README at Honeydew's current documentation: the workspace YAML schema page, and the Apache Ossie (OSI) page that documents this integration from the Honeydew side. Signed-off-by: Baruch Oxman --- converters/honeydew/README.md | 14 +- .../honeydew/src/ossie_honeydew/converter.py | 207 +++++++++--------- .../tests/test_ossie_honeydew_converter.py | 207 ++++++++++-------- 3 files changed, 226 insertions(+), 202 deletions(-) diff --git a/converters/honeydew/README.md b/converters/honeydew/README.md index b1b179e0..4419862b 100644 --- a/converters/honeydew/README.md +++ b/converters/honeydew/README.md @@ -19,14 +19,18 @@ # Apache Ossie ↔ Honeydew Converter -Bidirectional converter between [Apache Ossie](../../core-spec/spec.md) semantic models and [Honeydew](https://honeydew.ai/docs) workspace YAML. +Bidirectional converter between [Apache Ossie](../../core-spec/spec.md) semantic models +and [Honeydew](https://honeydew.ai/docs) [workspace YAML](https://honeydew.ai/docs/yaml-schema). + +Honeydew documents this integration from its own side under +[Apache Ossie (OSI)](https://honeydew.ai/docs/integration/apache-ossie-osi). ## Overview | Direction | Input | Output | |-----------|-------|--------| -| `osi-to-honeydew` | Single Ossie YAML file | Honeydew workspace directory | -| `honeydew-to-osi` | Honeydew workspace directory | Single Ossie YAML file | +| `ossie-to-honeydew` | Single Ossie YAML file | Honeydew workspace directory | +| `honeydew-to-ossie` | Honeydew workspace directory | Single Ossie YAML file | ### Ossie → Honeydew mapping @@ -70,10 +74,10 @@ uv sync ```bash # Ossie YAML → Honeydew workspace directory -uv run ossie-honeydew osi-to-honeydew -i input.yaml -o output_dir/ +uv run ossie-honeydew ossie-to-honeydew -i input.yaml -o output_dir/ # Honeydew workspace directory → Ossie YAML -uv run ossie-honeydew honeydew-to-osi -i workspace_dir/ -o output.yaml +uv run ossie-honeydew honeydew-to-ossie -i workspace_dir/ -o output.yaml ``` ## Tests diff --git a/converters/honeydew/src/ossie_honeydew/converter.py b/converters/honeydew/src/ossie_honeydew/converter.py index 1959b74e..33793e74 100644 --- a/converters/honeydew/src/ossie_honeydew/converter.py +++ b/converters/honeydew/src/ossie_honeydew/converter.py @@ -24,8 +24,8 @@ Honeydew → Ossie: Reads a Honeydew workspace directory and produces an Ossie YAML. Usage: - ossie-honeydew osi-to-honeydew -i input.yaml -o output_dir/ - ossie-honeydew honeydew-to-osi -i workspace_dir/ -o output.yaml + ossie-honeydew ossie-to-honeydew -i input.yaml -o output_dir/ + ossie-honeydew honeydew-to-ossie -i workspace_dir/ -o output.yaml """ import argparse @@ -38,9 +38,13 @@ import yaml -SUPPORTED_OSI_VERSION = "0.2.0.dev0" +SUPPORTED_OSSIE_VERSION = "0.2.0.dev0" HONEYDEW_VENDOR = "HONEYDEW" -_OSI_METADATA_SECTION = "osi" +_OSSIE_METADATA_SECTION = "ossie" +# Workspaces written before the Ossie rebrand named the section "osi". Still +# read it, so exporting such a workspace does not silently drop the fields it +# preserves (ai_context, label, unique_keys, custom_extensions, vendors). +_LEGACY_OSSIE_METADATA_SECTION = "osi" _HD_ATTR_KEYS = ("display_name", "hidden", "folder", "format_string", "timegrain") @@ -53,7 +57,7 @@ class HoneydewConversionError(Exception): # ───────────────────────────────────────────────────────────────────────────── -def convert_osi_to_honeydew(osi_yaml_str: str) -> dict[str, str]: +def convert_ossie_to_honeydew(ossie_yaml_str: str) -> dict[str, str]: """Convert an Ossie YAML string to a Honeydew workspace file tree. Returns a dict mapping relative file paths to their YAML content strings. @@ -73,10 +77,10 @@ def convert_osi_to_honeydew(osi_yaml_str: str) -> dict[str, str]: stored in ``metadata`` for lossless round-tripping. ``unique_keys`` and non-Honeydew ``custom_extensions`` have no direct Honeydew equivalent and are stored in the Honeydew ``metadata`` section under a section named - ``"osi"`` so they can be recovered on the return trip. + ``"ossie"`` so they can be recovered on the return trip. Args: - osi_yaml_str: Ossie YAML document as a string. + ossie_yaml_str: Ossie YAML document as a string. Returns: Dict of {relative_path: yaml_content}. @@ -84,14 +88,14 @@ def convert_osi_to_honeydew(osi_yaml_str: str) -> dict[str, str]: Raises: HoneydewConversionError: On invalid or unsupported input. """ - root = yaml.safe_load(osi_yaml_str) + root = yaml.safe_load(ossie_yaml_str) if not isinstance(root, dict): raise HoneydewConversionError("Invalid Ossie YAML: expected a mapping at the root") version_str = str(root.get("version", "")) - if version_str != SUPPORTED_OSI_VERSION: + if version_str != SUPPORTED_OSSIE_VERSION: raise HoneydewConversionError( - f"Unsupported Ossie version '{version_str}'. Supported: {SUPPORTED_OSI_VERSION}" + f"Unsupported Ossie version '{version_str}'. Supported: {SUPPORTED_OSSIE_VERSION}" ) semantic_models = root.get("semantic_model") @@ -122,7 +126,7 @@ def _model_to_files(sm: dict[str, Any], *, extra_vendors: list[str] | None = Non # Preserve model-level ai_context, non-HONEYDEW custom_extensions, and extra vendors model_ai_ctx = sm.get("ai_context") model_ext = [e for e in (sm.get("custom_extensions") or []) if e.get("vendor_name") != HONEYDEW_VENDOR] - ws_meta = _build_osi_metadata( + ws_meta = _build_ossie_metadata( ai_context=model_ai_ctx, custom_extensions=model_ext or None, extra_vendors=extra_vendors or None, @@ -180,7 +184,7 @@ def _fields_to_honeydew( if not expr or not expr.strip(): continue - datatype = _osi_field_to_honeydew_datatype(field) + datatype = _ossie_field_to_honeydew_datatype(field) field_desc = field.get("description") field_label = field.get("label") field_ai_ctx = field.get("ai_context") @@ -203,7 +207,7 @@ def _fields_to_honeydew( if syn not in labels: labels.append(syn) - field_meta = _build_osi_metadata( + field_meta = _build_ossie_metadata( ai_context=field_ai_ctx if isinstance(field_ai_ctx, dict) else None, label=field_label if field_label and not isinstance(field_ai_ctx, dict) else None, custom_extensions=field_ext or None, @@ -286,13 +290,13 @@ def _dataset_to_files( honeydew_relations = [] for rel in relations: - hr = _osi_relation_to_honeydew(rel) + hr = _ossie_relation_to_honeydew(rel) if hr is not None: honeydew_relations.append(hr) entity_dict["relations"] = honeydew_relations # Preserve Ossie fields that have no Honeydew native equivalent - entity_meta = _build_osi_metadata( + entity_meta = _build_ossie_metadata( ai_context=ai_context, unique_keys=unique_keys, custom_extensions=ds_ext or None, @@ -306,7 +310,7 @@ def _dataset_to_files( dataset_attrs, calc_attrs = _fields_to_honeydew(fields, entity_name) # ── dataset YAML ─────────────────────────────────────────────────────────── - source_sql, dataset_type = _parse_osi_source(ds.get("source", "")) + source_sql, dataset_type = _parse_ossie_source(ds.get("source", "")) dataset_dict: dict[str, Any] = { "type": "dataset", "entity": entity_name, @@ -349,7 +353,7 @@ def _dataset_to_files( metric_dict["description"] = f"{existing}\n{metric_ai_ctx}".strip() if existing else metric_ai_ctx metric_ext = [e for e in (metric.get("custom_extensions") or []) if e.get("vendor_name") != HONEYDEW_VENDOR] - metric_meta = _build_osi_metadata( + metric_meta = _build_ossie_metadata( ai_context=metric_ai_ctx, custom_extensions=metric_ext or None, ) @@ -366,7 +370,7 @@ def _dataset_to_files( return files -def _osi_relation_to_honeydew(rel: dict[str, Any]) -> dict[str, Any] | None: +def _ossie_relation_to_honeydew(rel: dict[str, Any]) -> dict[str, Any] | None: rel_name = rel.get("name", "") to_ds = rel.get("to") if not to_ds: @@ -437,7 +441,7 @@ def _pick_ansi_expression(expression: Any, field_name: str) -> str | None: return None -def _osi_field_to_honeydew_datatype(field: dict[str, Any]) -> str: +def _ossie_field_to_honeydew_datatype(field: dict[str, Any]) -> str: hd_ext = _get_honeydew_extension(field) if hd_ext.get("datatype"): return hd_ext["datatype"] @@ -453,7 +457,7 @@ def _is_simple_identifier(expr: str) -> bool: return bool(re.match(r"^[a-zA-Z_][a-zA-Z0-9_]*$", expr.strip())) -def _parse_osi_source(source: str) -> tuple[str, str]: +def _parse_ossie_source(source: str) -> tuple[str, str]: source = (source or "").strip() if not source: return ("", "table") @@ -528,7 +532,7 @@ def _find_entity_in_expression(expr: str, entity_names: set[str]) -> str | None: # ───────────────────────────────────────────────────────────────────────────── -def convert_honeydew_to_osi(workspace_dir: str) -> str: +def convert_honeydew_to_ossie(workspace_dir: str) -> str: """Convert a Honeydew workspace directory to an Ossie YAML string. Reads workspace.yml and all entity subdirectories under schema/. Honeydew @@ -554,7 +558,7 @@ def convert_honeydew_to_osi(workspace_dir: str) -> str: model_name = workspace.get("name") or os.path.basename(workspace_dir.rstrip("/\\")) model_description = workspace.get("description") - ws_osi_meta = _read_osi_metadata(workspace) + ws_ossie_meta = _read_ossie_metadata(workspace) schema_dir = os.path.join(workspace_dir, "schema") entity_dirs: list[str] = [] @@ -564,48 +568,48 @@ def convert_honeydew_to_osi(workspace_dir: str) -> str: if os.path.isdir(os.path.join(schema_dir, d)) ) - osi_datasets: list[dict[str, Any]] = [] - osi_relationships: list[dict[str, Any]] = [] - osi_metrics: list[dict[str, Any]] = [] + ossie_datasets: list[dict[str, Any]] = [] + ossie_relationships: list[dict[str, Any]] = [] + ossie_metrics: list[dict[str, Any]] = [] seen_relationships: set[tuple] = set() for entity_name in entity_dirs: entity_dir = os.path.join(schema_dir, entity_name) entity_data = _read_entity_dir(entity_dir, entity_name) - osi_datasets.append(_entity_to_osi_dataset(entity_data)) + ossie_datasets.append(_entity_to_ossie_dataset(entity_data)) for rel in entity_data["relations"]: - osi_rel = _honeydew_relation_to_osi( + ossie_rel = _honeydew_relation_to_ossie( rel, entity_name, seen_relationships ) - if osi_rel is not None: - osi_relationships.append(osi_rel) + if ossie_rel is not None: + ossie_relationships.append(ossie_rel) for metric in entity_data["metrics"]: - osi_m = _honeydew_metric_to_osi(metric, entity_name) - if osi_m is not None: - osi_metrics.append(osi_m) + ossie_m = _honeydew_metric_to_ossie(metric, entity_name) + if ossie_m is not None: + ossie_metrics.append(ossie_m) - sm: dict[str, Any] = {"name": model_name, "datasets": osi_datasets} + sm: dict[str, Any] = {"name": model_name, "datasets": ossie_datasets} if model_description: sm["description"] = str(model_description).strip() - if ws_osi_meta.get("ai_context"): - sm["ai_context"] = ws_osi_meta["ai_context"] + if ws_ossie_meta.get("ai_context"): + sm["ai_context"] = ws_ossie_meta["ai_context"] - restored_ws_ext = ws_osi_meta.get("custom_extensions") or [] + restored_ws_ext = ws_ossie_meta.get("custom_extensions") or [] if restored_ws_ext: sm["custom_extensions"] = restored_ws_ext - if osi_relationships: - sm["relationships"] = osi_relationships - if osi_metrics: - sm["metrics"] = osi_metrics + if ossie_relationships: + sm["relationships"] = ossie_relationships + if ossie_metrics: + sm["metrics"] = ossie_metrics - extra_vendors = ws_osi_meta.get("vendors") or [] + extra_vendors = ws_ossie_meta.get("vendors") or [] vendors = [HONEYDEW_VENDOR] + [v for v in extra_vendors if v != HONEYDEW_VENDOR] root: dict[str, Any] = { - "version": SUPPORTED_OSI_VERSION, + "version": SUPPORTED_OSSIE_VERSION, "vendors": vendors, "semantic_model": [sm], } @@ -622,7 +626,7 @@ def _read_entity_dir(entity_dir: str, entity_name: str) -> dict[str, Any]: "primary_dataset": None, "calculated_attributes": [], "metrics": [], - "osi_meta": {}, + "ossie_meta": {}, "honeydew_extra": {}, } @@ -634,7 +638,7 @@ def _read_entity_dir(entity_dir: str, entity_name: str) -> dict[str, Any]: data["description"] = ey.get("description") data["key_dataset"] = ey.get("key_dataset") data["relations"] = ey.get("relations") or [] - data["osi_meta"] = _read_osi_metadata(ey) + data["ossie_meta"] = _read_ossie_metadata(ey) data["honeydew_extra"] = { k: ey[k] for k in ("owner", "display_name", "hidden", "folder", "labels") if k in ey @@ -676,7 +680,7 @@ def _read_entity_dir(entity_dir: str, entity_name: str) -> dict[str, Any]: return data -def _entity_to_osi_dataset(entity_data: dict[str, Any]) -> dict[str, Any]: +def _entity_to_ossie_dataset(entity_data: dict[str, Any]) -> dict[str, Any]: entity_name = entity_data["name"] ds: dict[str, Any] = {"name": entity_name} @@ -691,21 +695,21 @@ def _entity_to_osi_dataset(entity_data: dict[str, Any]) -> dict[str, Any]: ds["primary_key"] = list(keys) # Restore Ossie-only fields preserved in Honeydew metadata - osi_meta = entity_data.get("osi_meta") or {} - if osi_meta.get("ai_context"): - ds["ai_context"] = osi_meta["ai_context"] + ossie_meta = entity_data.get("ossie_meta") or {} + if ossie_meta.get("ai_context"): + ds["ai_context"] = ossie_meta["ai_context"] # A Honeydew entity's keys uniquely identify its rows — Honeydew enforces this # and validates that relationships join to those keys — so surface them as a # unique key as well as the primary key. Union with any unique keys preserved # from an Ossie source, de-duplicated. - unique_keys = [list(uk) for uk in (osi_meta.get("unique_keys") or [])] + unique_keys = [list(uk) for uk in (ossie_meta.get("unique_keys") or [])] if keys and tuple(keys) not in {tuple(uk) for uk in unique_keys}: unique_keys.append(list(keys)) if unique_keys: ds["unique_keys"] = unique_keys - restored_ext = list(osi_meta.get("custom_extensions") or []) + restored_ext = list(ossie_meta.get("custom_extensions") or []) honeydew_extra = entity_data.get("honeydew_extra") or {} if honeydew_extra: restored_ext.append({"vendor_name": HONEYDEW_VENDOR, "data": json.dumps(honeydew_extra)}) @@ -729,28 +733,28 @@ def _entity_to_osi_dataset(entity_data: dict[str, Any]) -> dict[str, Any]: "expression": {"dialects": [{"dialect": "ANSI_SQL", "expression": col}]}, } datatype = attr.get("datatype") or "string" - dim = _honeydew_datatype_to_osi_dimension(datatype) + dim = _honeydew_datatype_to_ossie_dimension(datatype) if dim is not None: field["dimension"] = dim if attr.get("description"): field["description"] = str(attr["description"]).strip() - attr_osi_meta = _read_osi_metadata(attr) + attr_ossie_meta = _read_ossie_metadata(attr) attr_labels = attr.get("labels") or [] # Restore ai_context (structured form takes priority) # Only synthesise synonyms from labels when they are native Honeydew labels - # (i.e. osi_meta has no 'label' key, meaning the label didn't come from Ossie) - if attr_osi_meta.get("ai_context"): - field["ai_context"] = attr_osi_meta["ai_context"] - elif attr_labels and "label" not in attr_osi_meta: + # (i.e. ossie_meta has no 'label' key, meaning the label didn't come from Ossie) + if attr_ossie_meta.get("ai_context"): + field["ai_context"] = attr_ossie_meta["ai_context"] + elif attr_labels and "label" not in attr_ossie_meta: field["ai_context"] = {"synonyms": list(attr_labels)} - # Restore label: prefer osi_meta (exact round-trip), else first Honeydew label - # Don't set label when labels came from ai_context.synonyms (osi_meta has ai_context) - if "label" in attr_osi_meta: - field["label"] = attr_osi_meta["label"] - elif attr_labels and not attr_osi_meta.get("ai_context"): + # Restore label: prefer ossie_meta (exact round-trip), else first Honeydew label + # Don't set label when labels came from ai_context.synonyms (ossie_meta has ai_context) + if "label" in attr_ossie_meta: + field["label"] = attr_ossie_meta["label"] + elif attr_labels and not attr_ossie_meta.get("ai_context"): field["label"] = attr_labels[0] # Honeydew-specific metadata → HONEYDEW custom_extension @@ -763,7 +767,7 @@ def _entity_to_osi_dataset(entity_data: dict[str, Any]) -> dict[str, Any]: if len(attr_labels) > 1: attr_honeydew_extra["labels"] = attr_labels - all_ext = list(attr_osi_meta.get("custom_extensions") or []) + all_ext = list(attr_ossie_meta.get("custom_extensions") or []) if attr_honeydew_extra: all_ext.append({"vendor_name": HONEYDEW_VENDOR, "data": json.dumps(attr_honeydew_extra)}) if all_ext: @@ -784,7 +788,7 @@ def _entity_to_osi_dataset(entity_data: dict[str, Any]) -> dict[str, Any]: "name": aname, "expression": {"dialects": [{"dialect": "ANSI_SQL", "expression": sql}]}, } - dim = _honeydew_datatype_to_osi_dimension(datatype) + dim = _honeydew_datatype_to_ossie_dimension(datatype) if dim is not None: field["dimension"] = dim if calc.get("description"): @@ -792,17 +796,17 @@ def _entity_to_osi_dataset(entity_data: dict[str, Any]) -> dict[str, Any]: if cleaned: field["description"] = cleaned - calc_osi_meta = _read_osi_metadata(calc) + calc_ossie_meta = _read_ossie_metadata(calc) calc_labels = calc.get("labels") or [] - if calc_osi_meta.get("ai_context"): - field["ai_context"] = calc_osi_meta["ai_context"] - elif calc_labels and "label" not in calc_osi_meta: + if calc_ossie_meta.get("ai_context"): + field["ai_context"] = calc_ossie_meta["ai_context"] + elif calc_labels and "label" not in calc_ossie_meta: field["ai_context"] = {"synonyms": list(calc_labels)} - if "label" in calc_osi_meta: - field["label"] = calc_osi_meta["label"] - elif calc_labels and not calc_osi_meta.get("ai_context"): + if "label" in calc_ossie_meta: + field["label"] = calc_ossie_meta["label"] + elif calc_labels and not calc_ossie_meta.get("ai_context"): field["label"] = calc_labels[0] calc_honeydew_extra = { @@ -812,7 +816,7 @@ def _entity_to_osi_dataset(entity_data: dict[str, Any]) -> dict[str, Any]: if datatype == "bool": calc_honeydew_extra["datatype"] = datatype - all_calc_ext = list(calc_osi_meta.get("custom_extensions") or []) + all_calc_ext = list(calc_ossie_meta.get("custom_extensions") or []) # Always mark as calculated_attribute so Ossie → Honeydew routes it correctly all_calc_ext.append({ "vendor_name": HONEYDEW_VENDOR, @@ -828,7 +832,7 @@ def _entity_to_osi_dataset(entity_data: dict[str, Any]) -> dict[str, Any]: return ds -def _honeydew_datatype_to_osi_dimension(datatype: str) -> dict[str, Any] | None: +def _honeydew_datatype_to_ossie_dimension(datatype: str) -> dict[str, Any] | None: dt = (datatype or "").lower() if dt in ("date", "timestamp", "time"): return {"is_time": True} @@ -837,7 +841,7 @@ def _honeydew_datatype_to_osi_dimension(datatype: str) -> dict[str, Any] | None: return None # number / float → Ossie fact (no dimension key) -def _honeydew_relation_to_osi( +def _honeydew_relation_to_ossie( rel: dict[str, Any], entity_name: str, seen: set[tuple], @@ -871,21 +875,21 @@ def _honeydew_relation_to_osi( rel_name = rel.get("name") or f"{from_entity}_to_{to_entity}" - osi_rel: dict[str, Any] = {"name": rel_name, "from": from_entity, "to": to_entity} + ossie_rel: dict[str, Any] = {"name": rel_name, "from": from_entity, "to": to_entity} if from_cols: - osi_rel["from_columns"] = from_cols - osi_rel["to_columns"] = to_cols + ossie_rel["from_columns"] = from_cols + ossie_rel["to_columns"] = to_cols if connection_expr and not connection: sql_expr = (connection_expr.get("sql") or "") if isinstance(connection_expr, dict) else str(connection_expr) - osi_rel["custom_extensions"] = [ + ossie_rel["custom_extensions"] = [ {"vendor_name": HONEYDEW_VENDOR, "data": json.dumps({"connection_expr": sql_expr})} ] - return osi_rel + return ossie_rel -def _honeydew_metric_to_osi(metric: dict[str, Any], entity_name: str) -> dict[str, Any] | None: +def _honeydew_metric_to_ossie(metric: dict[str, Any], entity_name: str) -> dict[str, Any] | None: mname = metric.get("name") or "" if not mname: return None @@ -895,7 +899,7 @@ def _honeydew_metric_to_osi(metric: dict[str, Any], entity_name: str) -> dict[st warnings.warn(f"Metric '{mname}' in entity '{entity_name}' has no SQL, skipping") return None - osi_m: dict[str, Any] = { + ossie_m: dict[str, Any] = { "name": mname, "expression": {"dialects": [{"dialect": "ANSI_SQL", "expression": sql}]}, "custom_extensions": [ @@ -906,17 +910,17 @@ def _honeydew_metric_to_osi(metric: dict[str, Any], entity_name: str) -> dict[st if metric.get("description"): cleaned = str(metric["description"]).strip() if cleaned: - osi_m["description"] = cleaned + ossie_m["description"] = cleaned - metric_osi_meta = _read_osi_metadata(metric) - if metric_osi_meta.get("ai_context"): - osi_m["ai_context"] = metric_osi_meta["ai_context"] + metric_ossie_meta = _read_ossie_metadata(metric) + if metric_ossie_meta.get("ai_context"): + ossie_m["ai_context"] = metric_ossie_meta["ai_context"] - restored_ext = metric_osi_meta.get("custom_extensions") or [] + restored_ext = metric_ossie_meta.get("custom_extensions") or [] if restored_ext: - osi_m["custom_extensions"] = osi_m["custom_extensions"] + list(restored_ext) + ossie_m["custom_extensions"] = ossie_m["custom_extensions"] + list(restored_ext) - return osi_m + return ossie_m # ───────────────────────────────────────────────────────────────────────────── @@ -924,7 +928,7 @@ def _honeydew_metric_to_osi(metric: dict[str, Any], entity_name: str) -> dict[st # ───────────────────────────────────────────────────────────────────────────── -def _build_osi_metadata( +def _build_ossie_metadata( *, ai_context: Any = None, label: str | None = None, @@ -949,13 +953,16 @@ def _build_osi_metadata( if not items: return None - return {"name": _OSI_METADATA_SECTION, "metadata": items} + return {"name": _OSSIE_METADATA_SECTION, "metadata": items} -def _read_osi_metadata(obj: dict[str, Any]) -> dict[str, Any]: - """Read Ossie-preserved fields from a Honeydew object's 'osi' metadata section.""" +def _read_ossie_metadata(obj: dict[str, Any]) -> dict[str, Any]: + """Read Ossie-preserved fields from a Honeydew object's 'ossie' metadata section.""" for section in (obj.get("metadata") or []): - if (section.get("name") or "") != _OSI_METADATA_SECTION: + if (section.get("name") or "") not in ( + _OSSIE_METADATA_SECTION, + _LEGACY_OSSIE_METADATA_SECTION, + ): continue result: dict[str, Any] = {} for item in (section.get("metadata") or []): @@ -1022,21 +1029,21 @@ def main() -> None: ) sub = parser.add_subparsers(dest="command", required=True) - p1 = sub.add_parser("osi-to-honeydew", help="Convert Ossie YAML → Honeydew workspace") + p1 = sub.add_parser("ossie-to-honeydew", help="Convert Ossie YAML → Honeydew workspace") p1.add_argument("-i", "--input", required=True, help="Ossie YAML input file") p1.add_argument("-o", "--output", required=True, help="Output directory for Honeydew workspace") - p2 = sub.add_parser("honeydew-to-osi", help="Convert Honeydew workspace → Ossie YAML") + p2 = sub.add_parser("honeydew-to-ossie", help="Convert Honeydew workspace → Ossie YAML") p2.add_argument("-i", "--input", required=True, help="Honeydew workspace directory") p2.add_argument("-o", "--output", required=True, help="Ossie YAML output file") args = parser.parse_args() - if args.command == "osi-to-honeydew": + if args.command == "ossie-to-honeydew": with open(args.input) as f: - osi_yaml = f.read() + ossie_yaml = f.read() try: - files = convert_osi_to_honeydew(osi_yaml) + files = convert_ossie_to_honeydew(ossie_yaml) except HoneydewConversionError as e: print(f"Error: {e}", file=sys.stderr) sys.exit(1) @@ -1052,15 +1059,15 @@ def main() -> None: f.write(content) print(f"Wrote {len(files)} file(s) to {args.output}") - elif args.command == "honeydew-to-osi": + elif args.command == "honeydew-to-ossie": try: - osi_yaml = convert_honeydew_to_osi(args.input) + ossie_yaml = convert_honeydew_to_ossie(args.input) except HoneydewConversionError as e: print(f"Error: {e}", file=sys.stderr) sys.exit(1) with open(args.output, "w") as f: - f.write(osi_yaml) + f.write(ossie_yaml) print(f"Converted {args.input} → {args.output}") diff --git a/converters/honeydew/tests/test_ossie_honeydew_converter.py b/converters/honeydew/tests/test_ossie_honeydew_converter.py index 9eaa6733..d83523b4 100644 --- a/converters/honeydew/tests/test_ossie_honeydew_converter.py +++ b/converters/honeydew/tests/test_ossie_honeydew_converter.py @@ -29,36 +29,36 @@ from ossie_honeydew.converter import ( HoneydewConversionError, _assign_metrics_to_entities, - _build_osi_metadata, + _build_ossie_metadata, _check_safe_path, _fields_to_honeydew, _find_entity_in_expression, - _honeydew_datatype_to_osi_dimension, + _honeydew_datatype_to_ossie_dimension, _is_simple_identifier, - _osi_field_to_honeydew_datatype, - _parse_osi_source, + _ossie_field_to_honeydew_datatype, + _parse_ossie_source, _pick_ansi_expression, - _read_osi_metadata, - convert_honeydew_to_osi, - convert_osi_to_honeydew, + _read_ossie_metadata, + convert_honeydew_to_ossie, + convert_ossie_to_honeydew, ) # ───────────────────────────────────────────────────────────────────────────── # Helpers # ───────────────────────────────────────────────────────────────────────────── -OSI_VERSION = "0.2.0.dev0" +OSSIE_VERSION = "0.2.0.dev0" -def _osi(model_dict): +def _ossie(model_dict): return yaml.dump( - {"version": OSI_VERSION, "semantic_model": [model_dict]}, + {"version": OSSIE_VERSION, "semantic_model": [model_dict]}, default_flow_style=False, sort_keys=False, ) -def _minimal_osi_field(name, expr, is_dimension=True, is_time=False): +def _minimal_ossie_field(name, expr, is_dimension=True, is_time=False): field = { "name": name, "expression": {"dialects": [{"dialect": "ANSI_SQL", "expression": expr}]}, @@ -77,9 +77,9 @@ def _minimal_model(): "source": "db.schema.orders", "primary_key": ["order_id"], "fields": [ - _minimal_osi_field("order_id", "order_id"), - _minimal_osi_field("order_date", "order_date", is_time=True), - _minimal_osi_field("total", "total_amount", is_dimension=False), + _minimal_ossie_field("order_id", "order_id"), + _minimal_ossie_field("order_date", "order_date", is_time=True), + _minimal_ossie_field("total", "total_amount", is_dimension=False), ], } ], @@ -133,21 +133,21 @@ def _write_workspace(tmp_dir, workspace_name, entities): yaml.dump(m, f) -def _osi_roundtrip(model_dict, tmp_path): +def _ossie_roundtrip(model_dict, tmp_path): """Ossie → Honeydew → Ossie; returns the semantic model dict.""" - files = convert_osi_to_honeydew(_osi(model_dict)) + files = convert_ossie_to_honeydew(_ossie(model_dict)) for rel_path, content in files.items(): p = tmp_path / rel_path p.parent.mkdir(parents=True, exist_ok=True) p.write_text(content) - return yaml.safe_load(convert_honeydew_to_osi(str(tmp_path)))["semantic_model"][0] + return yaml.safe_load(convert_honeydew_to_ossie(str(tmp_path)))["semantic_model"][0] def _honeydew_roundtrip(entities, tmp_path): """Honeydew → Ossie → Honeydew; returns Path to the output workspace directory.""" _write_workspace(str(tmp_path), "ws", entities) - osi_yaml = convert_honeydew_to_osi(str(tmp_path)) - files = convert_osi_to_honeydew(osi_yaml) + ossie_yaml = convert_honeydew_to_ossie(str(tmp_path)) + files = convert_ossie_to_honeydew(ossie_yaml) out_dir = tmp_path / "out" for rel_path, content in files.items(): p = out_dir / rel_path @@ -177,8 +177,8 @@ def test_is_simple_identifier(expr, expected): ("WITH cte AS (SELECT 1) SELECT * FROM cte", "WITH cte AS (SELECT 1) SELECT * FROM cte", "sql"), ("", "", "table"), ]) -def test_parse_osi_source(source, expected_sql, expected_type): - sql, dtype = _parse_osi_source(source) +def test_parse_ossie_source(source, expected_sql, expected_type): + sql, dtype = _parse_ossie_source(source) assert sql == expected_sql and dtype == expected_type @@ -187,8 +187,8 @@ def test_parse_osi_source(source, expected_sql, expected_type): ({"dimension": {"is_time": False}}, "string"), ({}, "number"), ]) -def test_osi_field_to_honeydew_datatype(field, expected_dt): - assert _osi_field_to_honeydew_datatype(field) == expected_dt +def test_ossie_field_to_honeydew_datatype(field, expected_dt): + assert _ossie_field_to_honeydew_datatype(field) == expected_dt @pytest.mark.parametrize("datatype,expected_dim", [ @@ -199,8 +199,8 @@ def test_osi_field_to_honeydew_datatype(field, expected_dt): ("number", None), ("float", None), ]) -def test_honeydew_datatype_to_osi_dimension(datatype, expected_dim): - assert _honeydew_datatype_to_osi_dimension(datatype) == expected_dim +def test_honeydew_datatype_to_ossie_dimension(datatype, expected_dim): + assert _honeydew_datatype_to_ossie_dimension(datatype) == expected_dim @pytest.mark.parametrize("expr,entities,expected", [ @@ -248,42 +248,55 @@ def test_pick_ansi_expression_non_dict_warns(): # ───────────────────────────────────────────────────────────────────────────── def test_build_and_read_ai_context_string(): - section = _build_osi_metadata(ai_context="orders, purchases") - result = _read_osi_metadata({"metadata": [section]}) + section = _build_ossie_metadata(ai_context="orders, purchases") + result = _read_ossie_metadata({"metadata": [section]}) assert result["ai_context"] == "orders, purchases" def test_build_and_read_ai_context_dict(): ctx = {"instructions": "Use for sales", "synonyms": ["orders", "purchases"]} - section = _build_osi_metadata(ai_context=ctx) - result = _read_osi_metadata({"metadata": [section]}) + section = _build_ossie_metadata(ai_context=ctx) + result = _read_ossie_metadata({"metadata": [section]}) assert result["ai_context"] == ctx def test_build_and_read_unique_keys(): uks = [["col1", "col2"], ["col3"]] - section = _build_osi_metadata(unique_keys=uks) - result = _read_osi_metadata({"metadata": [section]}) + section = _build_ossie_metadata(unique_keys=uks) + result = _read_ossie_metadata({"metadata": [section]}) assert result["unique_keys"] == uks def test_build_and_read_custom_extensions(): exts = [{"vendor_name": "SNOWFLAKE", "data": '{"warehouse": "WH"}'}] - section = _build_osi_metadata(custom_extensions=exts) - result = _read_osi_metadata({"metadata": [section]}) + section = _build_ossie_metadata(custom_extensions=exts) + result = _read_ossie_metadata({"metadata": [section]}) assert result["custom_extensions"] == exts -def test_read_osi_metadata_no_osi_section(): - assert _read_osi_metadata({"metadata": [{"name": "other", "metadata": []}]}) == {} +def test_read_ossie_metadata_no_ossie_section(): + assert _read_ossie_metadata({"metadata": [{"name": "other", "metadata": []}]}) == {} -def test_read_osi_metadata_no_metadata(): - assert _read_osi_metadata({}) == {} +def test_read_legacy_osi_metadata_section(): + """Workspaces written before the rebrand named the section 'osi'; still read it.""" + section = _build_ossie_metadata( + ai_context={"synonyms": ["orders"]}, + unique_keys=[["col1"]], + custom_extensions=[{"vendor_name": "SNOWFLAKE", "data": "{}"}], + ) + legacy = {**section, "name": "osi"} + assert _read_ossie_metadata({"metadata": [legacy]}) == _read_ossie_metadata( + {"metadata": [section]} + ) + + +def test_read_ossie_metadata_no_metadata(): + assert _read_ossie_metadata({}) == {} -def test_build_osi_metadata_nothing_to_store(): - assert _build_osi_metadata() is None +def test_build_ossie_metadata_nothing_to_store(): + assert _build_ossie_metadata() is None # ───────────────────────────────────────────────────────────────────────────── @@ -418,7 +431,7 @@ def test_check_safe_path(rel_path, expected): "sql": "db.s.orders", "dataset_type": "table", "attributes": [{"column": "status", "name": "status", "datatype": "string", "labels": ["sales"], - "metadata": [{"name": "osi", "metadata": [ + "metadata": [{"name": "ossie", "metadata": [ {"name": "label", "value": "sales"} ]}]}], }, @@ -456,7 +469,7 @@ def test_check_safe_path(rel_path, expected): "column": "total", "name": "total", "datatype": "number", "description": "Use for revenue", "labels": ["rev", "earnings"], - "metadata": [{"name": "osi", "metadata": [ + "metadata": [{"name": "ossie", "metadata": [ {"name": "ai_context", "value": '{"instructions": "Use for revenue", "synonyms": ["rev", "earnings"]}'}, ]}], @@ -474,7 +487,7 @@ def test_check_safe_path(rel_path, expected): { "type": "entity", "name": "items", "keys": ["item_id"], "key_dataset": "items", "relations": [], - "metadata": [{"name": "osi", "metadata": [ + "metadata": [{"name": "ossie", "metadata": [ {"name": "unique_keys", "value": '[["sku"], ["item_id", "variant"]]'}, ]}], }, @@ -488,7 +501,7 @@ def test_check_safe_path(rel_path, expected): "schema/orders/orders.yml", { "type": "entity", "name": "orders", "key_dataset": "orders", "relations": [], - "metadata": [{"name": "osi", "metadata": [ + "metadata": [{"name": "ossie", "metadata": [ {"name": "custom_extensions", "value": '[{"vendor_name": "SNOWFLAKE", "data": "{\\"warehouse\\": \\"WH\\"}"}]'}, ]}], @@ -520,7 +533,7 @@ def test_check_safe_path(rel_path, expected): "workspace.yml", { "type": "workspace", "name": "m", - "metadata": [{"name": "osi", "metadata": [ + "metadata": [{"name": "ossie", "metadata": [ {"name": "ai_context", "value": '{"instructions": "Use for retail analytics", "synonyms": ["store"]}'}, ]}], @@ -559,13 +572,13 @@ def test_check_safe_path(rel_path, expected): id="composite-pk", ), ]) -def test_osi_to_honeydew_file_content(model, path, expected): - files = convert_osi_to_honeydew(_osi(model)) +def test_ossie_to_honeydew_file_content(model, path, expected): + files = convert_ossie_to_honeydew(_ossie(model)) assert path in files assert yaml.safe_load(files[path]) == expected -def test_osi_to_honeydew_metric_entity_hint_overrides_expression(): +def test_ossie_to_honeydew_metric_entity_hint_overrides_expression(): model = {"name": "m", "datasets": [ {"name": "orders", "source": "db.s.orders", "fields": []}, @@ -576,29 +589,29 @@ def test_osi_to_honeydew_metric_entity_hint_overrides_expression(): "expression": {"dialects": [{"dialect": "ANSI_SQL", "expression": "SUM(orders.x)"}]}, "custom_extensions": [{"vendor_name": "HONEYDEW", "data": '{"entity": "customers"}'}], }]} - files = convert_osi_to_honeydew(_osi(model)) + files = convert_ossie_to_honeydew(_ossie(model)) assert "schema/customers/metrics/cnt.yml" in files assert "schema/orders/metrics/cnt.yml" not in files -def test_osi_to_honeydew_invalid_version_raises(): +def test_ossie_to_honeydew_invalid_version_raises(): with pytest.raises(HoneydewConversionError, match="Unsupported"): - convert_osi_to_honeydew("version: '9.9.9'\nsemantic_model:\n - name: m\n") + convert_ossie_to_honeydew("version: '9.9.9'\nsemantic_model:\n - name: m\n") -def test_osi_to_honeydew_missing_semantic_model_raises(): +def test_ossie_to_honeydew_missing_semantic_model_raises(): with pytest.raises(HoneydewConversionError): - convert_osi_to_honeydew(f"version: '{OSI_VERSION}'\n") + convert_ossie_to_honeydew(f"version: '{OSSIE_VERSION}'\n") -def test_osi_to_honeydew_multiple_models_warns(): - doc = yaml.dump({"version": OSI_VERSION, "semantic_model": [ +def test_ossie_to_honeydew_multiple_models_warns(): + doc = yaml.dump({"version": OSSIE_VERSION, "semantic_model": [ {"name": "m1", "datasets": []}, {"name": "m2", "datasets": []}, ]}) with warnings.catch_warnings(record=True) as w: warnings.simplefilter("always") - files = convert_osi_to_honeydew(doc) + files = convert_ossie_to_honeydew(doc) assert any("only the first" in str(x.message) for x in w) assert yaml.safe_load(files["workspace.yml"]) == {"type": "workspace", "name": "m1"} @@ -608,7 +621,7 @@ def test_osi_to_honeydew_multiple_models_warns(): # ───────────────────────────────────────────────────────────────────────────── def _hd_root(sm): - return {"version": OSI_VERSION, "vendors": ["HONEYDEW"], "semantic_model": [sm]} + return {"version": OSSIE_VERSION, "vendors": ["HONEYDEW"], "semantic_model": [sm]} def _ansi(expr): @@ -786,35 +799,35 @@ def _ansi(expr): id="calc-attr", ), ]) -def test_honeydew_to_osi_output(tmp_path, ws_name, entities, expected_root): +def test_honeydew_to_ossie_output(tmp_path, ws_name, entities, expected_root): _write_workspace(str(tmp_path), ws_name, entities) - result = yaml.safe_load(convert_honeydew_to_osi(str(tmp_path))) + result = yaml.safe_load(convert_honeydew_to_ossie(str(tmp_path))) assert result == expected_root -def test_honeydew_to_osi_missing_workspace_raises(tmp_path): +def test_honeydew_to_ossie_missing_workspace_raises(tmp_path): with pytest.raises(HoneydewConversionError, match="workspace.yml"): - convert_honeydew_to_osi(str(tmp_path)) + convert_honeydew_to_ossie(str(tmp_path)) -def test_honeydew_to_osi_missing_schema_dir_empty_model(tmp_path): +def test_honeydew_to_ossie_missing_schema_dir_empty_model(tmp_path): (tmp_path / "workspace.yml").write_text(yaml.dump({"type": "workspace", "name": "ws"})) - result = yaml.safe_load(convert_honeydew_to_osi(str(tmp_path))) - assert result == {"version": OSI_VERSION, "vendors": ["HONEYDEW"], + result = yaml.safe_load(convert_honeydew_to_ossie(str(tmp_path))) + assert result == {"version": OSSIE_VERSION, "vendors": ["HONEYDEW"], "semantic_model": [{"name": "ws", "datasets": []}]} -def test_honeydew_to_osi_empty_metric_sql_skipped(tmp_path): +def test_honeydew_to_ossie_empty_metric_sql_skipped(tmp_path): _write_workspace(str(tmp_path), "ws", [{"name": "orders", "keys": ["id"], "key_dataset": "orders", "sql": "db.s.orders", "dataset_attrs": [], "metrics": [{"type": "metric", "entity": "orders", "name": "bad", "datatype": "number", "sql": ""}]}]) with warnings.catch_warnings(record=True): - result = yaml.safe_load(convert_honeydew_to_osi(str(tmp_path))) + result = yaml.safe_load(convert_honeydew_to_ossie(str(tmp_path))) assert "metrics" not in result["semantic_model"][0] -def test_honeydew_to_osi_duplicate_relations_deduplicated(tmp_path): +def test_honeydew_to_ossie_duplicate_relations_deduplicated(tmp_path): _write_workspace(str(tmp_path), "ws", [ {"name": "orders", "keys": ["id"], "key_dataset": "orders", "sql": "db.s.orders", "relations": [{"target_entity": "customers", "rel_type": "many-to-one", @@ -825,11 +838,11 @@ def test_honeydew_to_osi_duplicate_relations_deduplicated(tmp_path): "connection": [{"src_field": "id", "target_field": "cid"}]}], "dataset_attrs": []}, ]) - result = yaml.safe_load(convert_honeydew_to_osi(str(tmp_path))) + result = yaml.safe_load(convert_honeydew_to_ossie(str(tmp_path))) assert len(result["semantic_model"][0].get("relationships", [])) == 1 -def test_honeydew_to_osi_relation_target_columns_are_unique_keys(tmp_path): +def test_honeydew_to_ossie_relation_target_columns_are_unique_keys(tmp_path): # Honeydew validates that a many-to-one relation joins to the target entity's # keys, so the relation's to_columns are always covered by the target dataset's # unique_keys (derived from those keys). This preserves the cardinality metadata @@ -842,7 +855,7 @@ def test_honeydew_to_osi_relation_target_columns_are_unique_keys(tmp_path): {"name": "customers", "keys": ["id"], "key_dataset": "customers", "sql": "db.s.customers", "dataset_attrs": []}, ]) - sm = yaml.safe_load(convert_honeydew_to_osi(str(tmp_path)))["semantic_model"][0] + sm = yaml.safe_load(convert_honeydew_to_ossie(str(tmp_path)))["semantic_model"][0] datasets = {ds["name"]: ds for ds in sm["datasets"]} rel = sm["relationships"][0] target_ds = datasets[rel["to"]] @@ -972,24 +985,24 @@ def test_honeydew_to_osi_relation_target_columns_are_unique_keys(tmp_path): id="ai-context-string-becomes-desc", ), ]) -def test_osi_roundtrip_sm(tmp_path, model, expected_sm): - assert _osi_roundtrip(model, tmp_path) == expected_sm +def test_ossie_roundtrip_sm(tmp_path, model, expected_sm): + assert _ossie_roundtrip(model, tmp_path) == expected_sm -def test_osi_roundtrip_tpcds_example(tmp_path): +def test_ossie_roundtrip_tpcds_example(tmp_path): tpcds_path = ( Path(__file__).resolve().parent.parent.parent.parent / "examples" / "tpcds_semantic_model.yaml" ) if not tpcds_path.exists(): pytest.skip("TPC-DS example not found") - osi_yaml = tpcds_path.read_text() - files = convert_osi_to_honeydew(osi_yaml) + ossie_yaml = tpcds_path.read_text() + files = convert_ossie_to_honeydew(ossie_yaml) for rel_path, content in files.items(): p = tmp_path / rel_path p.parent.mkdir(parents=True, exist_ok=True) p.write_text(content) - result = yaml.safe_load(convert_honeydew_to_osi(str(tmp_path))) + result = yaml.safe_load(convert_honeydew_to_ossie(str(tmp_path))) sm = result["semantic_model"][0] assert sm["name"] == "tpcds_retail_model" ds_names = {ds["name"] for ds in sm["datasets"]} @@ -1049,7 +1062,7 @@ def test_osi_roundtrip_tpcds_example(tmp_path): "attributes": [{ "column": "status", "name": "status", "datatype": "string", "labels": ["sales"], - "metadata": [{"name": "osi", "metadata": [ + "metadata": [{"name": "ossie", "metadata": [ {"name": "ai_context", "value": '{"synonyms": ["sales"]}'}, ]}], }], @@ -1260,7 +1273,7 @@ def test_empty_or_whitespace_field_expression_skipped(expression): "expression": expression, "dimension": {"is_time": False}, }]}]} - files = convert_osi_to_honeydew(_osi(model)) + files = convert_ossie_to_honeydew(_ossie(model)) ds = yaml.safe_load(files["schema/orders/datasets/orders.yml"]) assert ds == {"type": "dataset", "entity": "orders", "name": "orders", "sql": "db.s.orders", "dataset_type": "table", "attributes": []} @@ -1275,7 +1288,7 @@ def test_empty_or_whitespace_metric_expression_skipped(expression): model = {"name": "m", "datasets": [{"name": "orders", "source": "db.s.orders", "fields": []}], "metrics": [{"name": "bad_m", "expression": expression}]} - files = convert_osi_to_honeydew(_osi(model)) + files = convert_ossie_to_honeydew(_ossie(model)) assert "schema/orders/metrics/bad_m.yml" not in files @@ -1287,7 +1300,7 @@ def test_non_dict_expression_warns(): }]}]} with warnings.catch_warnings(record=True) as w: warnings.simplefilter("always") - files = convert_osi_to_honeydew(_osi(model)) + files = convert_ossie_to_honeydew(_ossie(model)) assert any("must be a mapping" in str(x.message) for x in w) ds = yaml.safe_load(files["schema/orders/datasets/orders.yml"]) assert ds == {"type": "dataset", "entity": "orders", "name": "orders", @@ -1303,7 +1316,7 @@ def test_duplicate_metric_name_warns(): ]} with warnings.catch_warnings(record=True) as w: warnings.simplefilter("always") - files = convert_osi_to_honeydew(_osi(model)) + files = convert_ossie_to_honeydew(_ossie(model)) assert any("total" in str(x.message) for x in w) assert yaml.safe_load(files["schema/orders/metrics/total.yml"]) == { "type": "metric", "entity": "orders", "name": "total", @@ -1316,7 +1329,7 @@ def test_metric_string_ai_context_preserved_in_roundtrip(tmp_path): "datasets": [{"name": "orders", "source": "db.s.orders", "fields": []}], "metrics": [{"name": "rev", "ai_context": "Use for revenue analysis", "expression": {"dialects": [{"dialect": "ANSI_SQL", "expression": "SUM(orders.total)"}]}}]} - sm = _osi_roundtrip(model, tmp_path) + sm = _ossie_roundtrip(model, tmp_path) assert sm == { "name": "m", "datasets": [{"name": "orders", "source": "db.s.orders"}], @@ -1330,7 +1343,7 @@ def test_metric_string_ai_context_preserved_in_roundtrip(tmp_path): } -def test_malformed_osi_metadata_json_warns(tmp_path): +def test_malformed_ossie_metadata_json_warns(tmp_path): ws_path = tmp_path / "workspace.yml" ws_path.write_text(yaml.dump({"type": "workspace", "name": "ws"})) base = tmp_path / "schema" / "orders" @@ -1338,7 +1351,7 @@ def test_malformed_osi_metadata_json_warns(tmp_path): entity = { "type": "entity", "name": "orders", "keys": ["id"], "key_dataset": "orders", "relations": [], - "metadata": [{"name": "osi", "metadata": [ + "metadata": [{"name": "ossie", "metadata": [ {"name": "unique_keys", "value": "[broken json"}, ]}], } @@ -1349,7 +1362,7 @@ def test_malformed_osi_metadata_json_warns(tmp_path): )) with warnings.catch_warnings(record=True) as w: warnings.simplefilter("always") - convert_honeydew_to_osi(str(tmp_path)) + convert_honeydew_to_ossie(str(tmp_path)) assert any("unique_keys" in str(x.message) for x in w) @@ -1396,7 +1409,7 @@ def test_connectionless_relation_warns(): ], "relationships": [{"name": "r", "from": "orders", "to": "customers"}]} with warnings.catch_warnings(record=True) as w: warnings.simplefilter("always") - files = convert_osi_to_honeydew(_osi(model)) + files = convert_ossie_to_honeydew(_ossie(model)) assert any("resolve the join" in str(x.message) for x in w) assert yaml.safe_load(files["schema/orders/orders.yml"]) == { "type": "entity", "name": "orders", "key_dataset": "orders", @@ -1415,17 +1428,17 @@ def test_connectionless_relation_warns(): ]) def test_vendors_roundtrip(tmp_path, input_vendors, expected_vendors): doc = yaml.dump({ - "version": OSI_VERSION, + "version": OSSIE_VERSION, "vendors": input_vendors, "semantic_model": [{"name": "m", "datasets": []}], }) - files = convert_osi_to_honeydew(doc) + files = convert_ossie_to_honeydew(doc) for rel_path, content in files.items(): p = tmp_path / rel_path p.parent.mkdir(parents=True, exist_ok=True) p.write_text(content) - result = yaml.safe_load(convert_honeydew_to_osi(str(tmp_path))) - assert result == {"version": OSI_VERSION, "vendors": expected_vendors, + result = yaml.safe_load(convert_honeydew_to_ossie(str(tmp_path))) + assert result == {"version": OSSIE_VERSION, "vendors": expected_vendors, "semantic_model": [{"name": "m", "datasets": []}]} @@ -1433,11 +1446,11 @@ def test_vendors_roundtrip(tmp_path, input_vendors, expected_vendors): # main() CLI smoke tests # ───────────────────────────────────────────────────────────────────────────── -def test_main_osi_to_honeydew(tmp_path): +def test_main_ossie_to_honeydew(tmp_path): import subprocess input_file = tmp_path / "model.yaml" input_file.write_text(yaml.dump({ - "version": OSI_VERSION, + "version": OSSIE_VERSION, "semantic_model": [{"name": "m", "datasets": [ {"name": "orders", "source": "db.s.orders", "fields": []} ]}], @@ -1445,7 +1458,7 @@ def test_main_osi_to_honeydew(tmp_path): output_dir = tmp_path / "out" result = subprocess.run( [sys.executable, "-m", "ossie_honeydew.converter", - "osi-to-honeydew", "-i", str(input_file), "-o", str(output_dir)], + "ossie-to-honeydew", "-i", str(input_file), "-o", str(output_dir)], capture_output=True, text=True, ) assert result.returncode == 0 @@ -1454,7 +1467,7 @@ def test_main_osi_to_honeydew(tmp_path): } -def test_main_honeydew_to_osi(tmp_path): +def test_main_honeydew_to_ossie(tmp_path): import subprocess _write_workspace(str(tmp_path), "ws", [{ "name": "orders", "keys": ["id"], "key_dataset": "orders", @@ -1463,12 +1476,12 @@ def test_main_honeydew_to_osi(tmp_path): output_file = tmp_path / "output.yaml" result = subprocess.run( [sys.executable, "-m", "ossie_honeydew.converter", - "honeydew-to-osi", "-i", str(tmp_path), "-o", str(output_file)], + "honeydew-to-ossie", "-i", str(tmp_path), "-o", str(output_file)], capture_output=True, text=True, ) assert result.returncode == 0 assert yaml.safe_load(output_file.read_text()) == { - "version": OSI_VERSION, + "version": OSSIE_VERSION, "vendors": ["HONEYDEW"], "semantic_model": [{"name": "ws", "datasets": [ {"name": "orders", "source": "DB.S.ORDERS", "primary_key": ["id"], @@ -1481,14 +1494,14 @@ def test_main_path_traversal_rejected(tmp_path): import subprocess input_file = tmp_path / "model.yaml" input_file.write_text( - f"version: '{OSI_VERSION}'\nsemantic_model:\n" + f"version: '{OSSIE_VERSION}'\nsemantic_model:\n" " - name: m\n datasets:\n" " - name: '../../evil'\n source: db.s.evil\n fields: []\n" ) output_dir = tmp_path / "out" result = subprocess.run( [sys.executable, "-m", "ossie_honeydew.converter", - "osi-to-honeydew", "-i", str(input_file), "-o", str(output_dir)], + "ossie-to-honeydew", "-i", str(input_file), "-o", str(output_dir)], capture_output=True, text=True, ) assert result.returncode == 1