diff --git a/README.md b/README.md index eee15c8a..94ed3f38 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ Apache Ossie provides a single JSON- and YAML-based specification that any tool ## What's in this repository -- [`core-spec/`](core-spec/) — The Ossie core specification (`spec.md`), the machine-readable schema (`spec.yaml`, `osi-schema.json`), and accompanying documentation. +- [`core-spec/`](core-spec/) — The Ossie core specification (`spec.md`), the machine-readable schema (`spec.yaml`, `ossie-schema.json`), and accompanying documentation. - [`converters/`](converters/) — Reference converters that translate between Ossie and other semantic formats (e.g., dbt, GoodData, Polaris, Salesforce). - [`examples/`](examples/) — Example semantic models, including a complete TPC-DS model. - [`validation/`](validation/) — Tooling for validating semantic models against the Ossie schema. diff --git a/ROADMAP.md b/ROADMAP.md index 72d564c4..a50068bc 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -268,7 +268,7 @@ Enterprise adoption requires consistent identifiers, validation, and governance **Related Issues:** -- [Issue #102 — Add semantic versioning and Git releases for core-spec/osi-schema.json](https://github.com/apache/ossie/issues/102) +- [Issue #102 — Add semantic versioning and Git releases for core-spec/ossie-schema.json](https://github.com/apache/ossie/issues/102) - [Issue #92 — Community Implementation: Trust Control Center — Ossie-compatible governance & reconciliation platform](https://github.com/apache/ossie/issues/92) - [Issue #87 — New Flags: Restricted and Internal only indicators](https://github.com/apache/ossie/issues/87) @@ -428,7 +428,7 @@ Broad ecosystem adoption depends on practical tools that let teams validate thei **Existing Artifacts:** -- [JSON Schema (osi-schema.json)](core-spec/osi-schema.json) — schema for structural validation +- [JSON Schema (ossie-schema.json)](core-spec/ossie-schema.json) — schema for structural validation - [Validation Script (validate.py)](validation/validate.py) — validates Ossie YAML against JSON Schema, unique names, references, and SQL syntax - [Snowflake Converter](converters/snowflake/) — Ossie → Snowflake Cortex Analyst YAML converter - [GoodData Converter](converters/gooddata/) — bidirectional Ossie ↔ GoodData LDM converter diff --git a/converters/README.md b/converters/README.md index ac31d856..b16f2b65 100644 --- a/converters/README.md +++ b/converters/README.md @@ -228,7 +228,7 @@ A converter should map `ai_context` when the target vendor supports equivalent c ### Step-by-Step Guide -1. **Validate input**: Use the [Ossie JSON Schema](../core-spec/osi-schema.json) and the [validation script](../validation/validate.py) to ensure the source Ossie model is valid before conversion. +1. **Validate input**: Use the [Ossie JSON Schema](../core-spec/ossie-schema.json) and the [validation script](../validation/validate.py) to ensure the source Ossie model is valid before conversion. 2. **Parse the Ossie model**: Load the YAML file and iterate over the top-level `semantic_model` entries. diff --git a/converters/databricks/pyproject.toml b/converters/databricks/pyproject.toml index d4ed3511..9288975f 100644 --- a/converters/databricks/pyproject.toml +++ b/converters/databricks/pyproject.toml @@ -25,7 +25,7 @@ version = "0.2.0.dev0" description = "Databricks Unity Catalog Metric View <> Apache Ossie converter" requires-python = ">=3.11" classifiers = [ - "License :: OSI Approved :: Apache Software License", + "License :: Ossie Approved :: Apache Software License", "Programming Language :: Python :: 3", ] dependencies = [ diff --git a/converters/databricks/tests/fixtures/fixtureA_ossie.yaml b/converters/databricks/tests/fixtures/fixtureA_ossie.yaml index a53942f3..49baf43b 100644 --- a/converters/databricks/tests/fixtures/fixtureA_ossie.yaml +++ b/converters/databricks/tests/fixtures/fixtureA_ossie.yaml @@ -15,7 +15,7 @@ # specific language governing permissions and limitations # under the License. -# yaml-language-server: $schema=../../../../core-spec/osi-schema.json +# yaml-language-server: $schema=../../../../core-spec/ossie-schema.json # # Fixture A -- all-native round-trip (Apache Ossie -> MV -> Apache Ossie). # Star schema; every field maps to a native MV field. Documented losses on the diff --git a/converters/databricks/tests/fixtures/fixtureB_ossie.yaml b/converters/databricks/tests/fixtures/fixtureB_ossie.yaml index f2c11d9c..7fa53dfd 100644 --- a/converters/databricks/tests/fixtures/fixtureB_ossie.yaml +++ b/converters/databricks/tests/fixtures/fixtureB_ossie.yaml @@ -15,7 +15,7 @@ # specific language governing permissions and limitations # under the License. -# yaml-language-server: $schema=../../../../core-spec/osi-schema.json +# yaml-language-server: $schema=../../../../core-spec/ossie-schema.json # # Fixture B -- expected Apache Ossie produced from fixtureB_metric_view.yaml. MV-only # features are stashed in custom_extensions[DATABRICKS], keyed by their exact v1.1 diff --git a/converters/databricks/tests/test_roundtrip_properties.py b/converters/databricks/tests/test_roundtrip_properties.py index d022ebb1..b22d4a1c 100644 --- a/converters/databricks/tests/test_roundtrip_properties.py +++ b/converters/databricks/tests/test_roundtrip_properties.py @@ -100,7 +100,7 @@ def test_mv_to_ossie_to_mv(self, mv): assert_mv_roundtrip(mv) -class TestOSIRoundTrip: +class TestOssieRoundTrip: @given(ossie=ossie_models()) @_SETTINGS def test_ossie_to_mv_to_ossie(self, ossie): diff --git a/converters/dbt/README.md b/converters/dbt/README.md index c39090fb..623740f0 100644 --- a/converters/dbt/README.md +++ b/converters/dbt/README.md @@ -23,8 +23,8 @@ Converts between dbt's [MetricFlow Semantic Interface](https://docs.getdbt.com/d Both conversion directions are supported: -- `msi-to-osi` — `semantic_manifest.json` (dbt output) → Ossie YAML -- `osi-to-msi` — Ossie YAML → `semantic_manifest.json` +- `msi-to-ossie` — `semantic_manifest.json` (dbt output) → Ossie YAML +- `ossie-to-msi` — Ossie YAML → `semantic_manifest.json` ## Requirements @@ -57,13 +57,13 @@ dbt parse Then convert to Ossie YAML: ```bash -ossie-dbt msi-to-osi -i target/semantic_manifest.json -o semantic_model.yaml +ossie-dbt msi-to-ossie -i target/semantic_manifest.json -o semantic_model.yaml ``` By default the Ossie semantic model is named `semantic_model`. Override it with `--model-name`: ```bash -ossie-dbt msi-to-osi -i target/semantic_manifest.json -o semantic_model.yaml --model-name my_project +ossie-dbt msi-to-ossie -i target/semantic_manifest.json -o semantic_model.yaml --model-name my_project ``` Conversion issues (e.g. dropped CONVERSION or PRIVATE metrics) are printed as warnings to stderr. The output file is still written. @@ -71,7 +71,7 @@ Conversion issues (e.g. dropped CONVERSION or PRIVATE metrics) are printed as wa ### Apache Ossie → dbt ```bash -ossie-dbt osi-to-msi -i semantic_model.yaml -o semantic_manifest.json +ossie-dbt ossie-to-msi -i semantic_model.yaml -o semantic_manifest.json ``` Produces a `semantic_manifest.json` that metricflow can load. @@ -80,31 +80,31 @@ Produces a `semantic_manifest.json` that metricflow can load. ```bash ossie-dbt --help -ossie-dbt msi-to-osi --help -ossie-dbt osi-to-msi --help +ossie-dbt msi-to-ossie --help +ossie-dbt ossie-to-msi --help ``` ## Python API ```python -from ossie_dbt import MSIToOSIConverter, OSIToMSIConverter +from ossie_dbt import MSIToOssieConverter, OssieToMSIConverter from metricflow_semantics.model.dbt_manifest_parser import parse_manifest_from_dbt_generated_manifest # dbt → Apache Ossie manifest = parse_manifest_from_dbt_generated_manifest(Path("target/semantic_manifest.json").read_text()) -result = MSIToOSIConverter().convert(manifest, osi_model_name="my_project") +result = MSIToOssieConverter().convert(manifest, ossie_model_name="my_project") for issue in result.issues: print(f"[warning] {issue.issue_type.value}: {issue.element_name}") -osi_yaml = result.output.to_osi_yaml() +ossie_yaml = result.output.to_ossie_yaml() # Apache Ossie → dbt import yaml -from ossie import OSIDocument +from ossie import OssieDocument -document = OSIDocument.model_validate(yaml.safe_load(Path("semantic_model.yaml").read_text())) -result = OSIToMSIConverter().convert(document) +document = OssieDocument.model_validate(yaml.safe_load(Path("semantic_model.yaml").read_text())) +result = OssieToMSIConverter().convert(document) manifest_json = result.output.model_dump_json(by_alias=True, exclude_none=True, indent=2) ``` diff --git a/converters/dbt/src/ossie_dbt/__init__.py b/converters/dbt/src/ossie_dbt/__init__.py index f3f24928..96e2a6d1 100644 --- a/converters/dbt/src/ossie_dbt/__init__.py +++ b/converters/dbt/src/ossie_dbt/__init__.py @@ -16,13 +16,13 @@ # under the License. from ossie_dbt.converter_issues import ConverterIssue, ConverterIssueType, ConverterResult -from ossie_dbt.msi_to_osi import MSIToOSIConverter -from ossie_dbt.osi_to_msi import OSIToMSIConverter +from ossie_dbt.msi_to_ossie import MSIToOssieConverter +from ossie_dbt.ossie_to_msi import OssieToMSIConverter __all__ = [ "ConverterIssue", "ConverterIssueType", "ConverterResult", - "MSIToOSIConverter", - "OSIToMSIConverter", + "MSIToOssieConverter", + "OssieToMSIConverter", ] diff --git a/converters/dbt/src/ossie_dbt/cli.py b/converters/dbt/src/ossie_dbt/cli.py index 8d71faad..4daff7e1 100644 --- a/converters/dbt/src/ossie_dbt/cli.py +++ b/converters/dbt/src/ossie_dbt/cli.py @@ -18,8 +18,8 @@ """CLI entry point for the ossie-dbt converter. Usage: - ossie-dbt msi-to-osi -i semantic_manifest.json -o output.yaml - ossie-dbt osi-to-msi -i input.yaml -o semantic_manifest.json + ossie-dbt msi-to-ossie -i semantic_manifest.json -o output.yaml + ossie-dbt ossie-to-msi -i input.yaml -o semantic_manifest.json """ import argparse @@ -29,10 +29,10 @@ import yaml -from ossie import OSIDocument +from ossie import OssieDocument from ossie_dbt.converter_issues import ConverterIssueType -from ossie_dbt.msi_to_osi import MSIToOSIConverter -from ossie_dbt.osi_to_msi import OSIToMSIConverter +from ossie_dbt.msi_to_ossie import MSIToOssieConverter +from ossie_dbt.ossie_to_msi import OssieToMSIConverter from metricflow_semantics.model.dbt_manifest_parser import parse_manifest_from_dbt_generated_manifest @@ -50,12 +50,12 @@ } -def _cmd_msi_to_osi(args: argparse.Namespace) -> None: +def _cmd_msi_to_ossie(args: argparse.Namespace) -> None: input_path = Path(args.input) output_path = Path(args.output) manifest = parse_manifest_from_dbt_generated_manifest(input_path.read_text()) - result = MSIToOSIConverter().convert(manifest, osi_model_name=args.model_name) + result = MSIToOssieConverter().convert(manifest, ossie_model_name=args.model_name) if result.issues: for issue in result.issues: @@ -63,17 +63,17 @@ def _cmd_msi_to_osi(args: argparse.Namespace) -> None: reason = _ISSUE_REASON[issue.issue_type] print(f"[WARNING] {issue.issue_type.value}: {issue.element_name} {verb} during conversion because {reason}", file=sys.stderr) - output_path.write_text(result.output.to_osi_yaml()) + output_path.write_text(result.output.to_ossie_yaml()) print(f"Written to {output_path}", file=sys.stderr) -def _cmd_osi_to_msi(args: argparse.Namespace) -> None: +def _cmd_ossie_to_msi(args: argparse.Namespace) -> None: input_path = Path(args.input) output_path = Path(args.output) raw = yaml.safe_load(input_path.read_text()) - document = OSIDocument.model_validate(raw) - result = OSIToMSIConverter().convert(document) + document = OssieDocument.model_validate(raw) + result = OssieToMSIConverter().convert(document) output_path.write_text(result.output.model_dump_json(by_alias=True, exclude_none=True, indent=2)) print(f"Written to {output_path}", file=sys.stderr) @@ -86,22 +86,22 @@ def main() -> None: ) subparsers = parser.add_subparsers(dest="command", required=True) - msi_to_osi = subparsers.add_parser("msi-to-osi", help="Convert semantic_manifest.json → Ossie YAML") - msi_to_osi.add_argument("-i", "--input", required=True, metavar="FILE", help="Path to semantic_manifest.json") - msi_to_osi.add_argument("-o", "--output", required=True, metavar="FILE", help="Path for output Ossie YAML") - msi_to_osi.add_argument( + msi_to_ossie = subparsers.add_parser("msi-to-ossie", help="Convert semantic_manifest.json → Ossie YAML") + msi_to_ossie.add_argument("-i", "--input", required=True, metavar="FILE", help="Path to semantic_manifest.json") + msi_to_ossie.add_argument("-o", "--output", required=True, metavar="FILE", help="Path for output Ossie YAML") + msi_to_ossie.add_argument( "--model-name", default="semantic_model", metavar="NAME", help="Ossie semantic model name (default: semantic_model)" ) - osi_to_msi = subparsers.add_parser("osi-to-msi", help="Convert Ossie YAML → semantic_manifest.json") - osi_to_msi.add_argument("-i", "--input", required=True, metavar="FILE", help="Path to Ossie YAML") - osi_to_msi.add_argument("-o", "--output", required=True, metavar="FILE", help="Path for output semantic_manifest.json") + ossie_to_msi = subparsers.add_parser("ossie-to-msi", help="Convert Ossie YAML → semantic_manifest.json") + ossie_to_msi.add_argument("-i", "--input", required=True, metavar="FILE", help="Path to Ossie YAML") + ossie_to_msi.add_argument("-o", "--output", required=True, metavar="FILE", help="Path for output semantic_manifest.json") args = parser.parse_args() - if args.command == "msi-to-osi": - _cmd_msi_to_osi(args) - elif args.command == "osi-to-msi": - _cmd_osi_to_msi(args) + if args.command == "msi-to-ossie": + _cmd_msi_to_ossie(args) + elif args.command == "ossie-to-msi": + _cmd_ossie_to_msi(args) if __name__ == "__main__": diff --git a/converters/dbt/src/ossie_dbt/msi_to_osi.py b/converters/dbt/src/ossie_dbt/msi_to_ossie.py similarity index 92% rename from converters/dbt/src/ossie_dbt/msi_to_osi.py rename to converters/dbt/src/ossie_dbt/msi_to_ossie.py index db9e1315..3132cdcb 100644 --- a/converters/dbt/src/ossie_dbt/msi_to_osi.py +++ b/converters/dbt/src/ossie_dbt/msi_to_ossie.py @@ -22,16 +22,16 @@ from typing import Dict, List, Optional, Sequence, Tuple from ossie import ( - OSIDataset, - OSIDialect, - OSIDialectExpression, - OSIDimension, - OSIDocument, - OSIExpression, - OSIField, - OSIMetric, - OSIRelationship, - OSISemanticModel, + OssieDataset, + OssieDialect, + OssieDialectExpression, + OssieDimension, + OssieDocument, + OssieExpression, + OssieField, + OssieMetric, + OssieRelationship, + OssieSemanticModel, ) from ossie_dbt.converter_issues import ConverterIssue, ConverterIssueType, ConverterResult from ossie_dbt.filter_utils import _collect_filter_sql, _merge_filter_sqls @@ -73,15 +73,15 @@ class _RelationshipDirection: to_col: str -class MSIToOSIConverter: +class MSIToOssieConverter: """Converts an MSI SemanticManifest into an Ossie Document.""" - def __init__(self, dialect: OSIDialect = OSIDialect.ANSI_SQL) -> None: + def __init__(self, dialect: OssieDialect = OssieDialect.ANSI_SQL) -> None: self._dialect = dialect def convert( - self, manifest: PydanticSemanticManifest, osi_model_name: str = "semantic_model" - ) -> ConverterResult[OSIDocument]: + self, manifest: PydanticSemanticManifest, ossie_model_name: str = "semantic_model" + ) -> ConverterResult[OssieDocument]: manifest = PydanticSemanticManifestTransformer.transform(manifest) issues: List[ConverterIssue] = [] @@ -94,7 +94,7 @@ def convert( metric_index = self._build_metric_index(manifest.metrics) expression_cache: Dict[Tuple[str, Optional[str]], str] = {} - osi_metrics: List[OSIMetric] = [] + ossie_metrics: List[OssieMetric] = [] for metric in manifest.metrics: if metric.type is MetricType.CONVERSION: issues.append( @@ -111,8 +111,8 @@ def convert( ConverterIssue(issue_type=ConverterIssueType.CUMULATIVE_SEMANTICS_LOSS, element_name=metric.name) ) expr = self._resolve_metric_expression(metric, metric_index, expression_cache) - osi_metrics.append( - OSIMetric( + ossie_metrics.append( + OssieMetric( name=metric.name, expression=self._make_expression(expr), description=metric.description, @@ -120,23 +120,23 @@ def convert( ) return ConverterResult( - output=OSIDocument( + output=OssieDocument( version="0.2.0.dev0", dialects=[self._dialect], semantic_model=[ - OSISemanticModel( - name=osi_model_name, + OssieSemanticModel( + name=ossie_model_name, datasets=datasets, relationships=relationships if relationships else None, - metrics=osi_metrics if osi_metrics else None, + metrics=ossie_metrics if ossie_metrics else None, ) ], ), issues=issues, ) - def _convert_semantic_model(self, sm: SemanticModel) -> OSIDataset: - fields: List[OSIField] = [] + def _convert_semantic_model(self, sm: SemanticModel) -> OssieDataset: + fields: List[OssieField] = [] for entity in sm.entities: fields.append(self._convert_entity(entity)) for dim in sm.dimensions: @@ -146,7 +146,7 @@ def _convert_semantic_model(self, sm: SemanticModel) -> OSIDataset: primary_key, unique_keys = self._extract_keys(sm.entities) - return OSIDataset( + return OssieDataset( name=sm.name, source=sm.node_relation.relation_name, primary_key=primary_key, @@ -155,32 +155,32 @@ def _convert_semantic_model(self, sm: SemanticModel) -> OSIDataset: fields=fields if fields else None, ) - def _convert_dimension(self, dim: Dimension) -> OSIField: + def _convert_dimension(self, dim: Dimension) -> OssieField: expr = dim.expr if dim.expr is not None else dim.name is_time = dim.type is DimensionType.TIME - return OSIField( + return OssieField( name=dim.name, expression=self._make_expression(expr), - dimension=OSIDimension(is_time=is_time), + dimension=OssieDimension(is_time=is_time), label=dim.label, description=dim.description, ) - def _convert_entity(self, entity: Entity) -> OSIField: + def _convert_entity(self, entity: Entity) -> OssieField: expr = entity.expr if entity.expr is not None else entity.name - return OSIField( + return OssieField( name=entity.name, expression=self._make_expression(expr), label=entity.label, description=entity.description, ) - def _convert_measure(self, measure: Measure) -> OSIField: + def _convert_measure(self, measure: Measure) -> OssieField: expr = measure.expr if measure.expr is not None else measure.name - return OSIField( + return OssieField( name=measure.name, expression=self._make_expression(expr), label=measure.label, @@ -394,17 +394,17 @@ def _relationship_direction( @staticmethod def _build_relationships( entity_index: Dict[str, List[_EntityEntry]], - ) -> List[OSIRelationship]: + ) -> List[OssieRelationship]: """Resolve implicit MSI entity links into explicit Ossie relationships. Every pair of datasets sharing an entity name is a valid join path. """ - relationships: List[OSIRelationship] = [] + relationships: List[OssieRelationship] = [] for entity_name, entries in entity_index.items(): for entry_a, entry_b in combinations(entries, 2): if entry_a.dataset == entry_b.dataset: continue - direction = MSIToOSIConverter._relationship_direction( + direction = MSIToOssieConverter._relationship_direction( entry_a.dataset, entry_a.col, entry_a.entity_type, @@ -413,7 +413,7 @@ def _build_relationships( entry_b.entity_type, ) relationships.append( - OSIRelationship( + OssieRelationship( name=f"{direction.from_dataset}__{direction.to_dataset}__{entity_name}", from_dataset=direction.from_dataset, to=direction.to_dataset, @@ -462,5 +462,5 @@ def _build_agg_expression( else: assert_values_exhausted(agg) - def _make_expression(self, expr: str) -> OSIExpression: - return OSIExpression(dialects=[OSIDialectExpression(dialect=self._dialect, expression=expr)]) + def _make_expression(self, expr: str) -> OssieExpression: + return OssieExpression(dialects=[OssieDialectExpression(dialect=self._dialect, expression=expr)]) diff --git a/converters/dbt/src/ossie_dbt/osi_to_msi.py b/converters/dbt/src/ossie_dbt/ossie_to_msi.py similarity index 92% rename from converters/dbt/src/ossie_dbt/osi_to_msi.py rename to converters/dbt/src/ossie_dbt/ossie_to_msi.py index d6116feb..3350bac4 100644 --- a/converters/dbt/src/ossie_dbt/osi_to_msi.py +++ b/converters/dbt/src/ossie_dbt/ossie_to_msi.py @@ -19,12 +19,12 @@ from typing import List, Optional, Set from ossie import ( - OSIDataset, - OSIDialect, - OSIDocument, - OSIExpression, - OSIField, - OSISemanticModel, + OssieDataset, + OssieDialect, + OssieDocument, + OssieExpression, + OssieField, + OssieSemanticModel, ) from ossie_dbt.converter_issues import ConverterResult from ossie_dbt.expression_utils import ( @@ -74,7 +74,7 @@ class _KeySets: foreign: Set[str] -class OSIToMSIConverter: +class OssieToMSIConverter: """Converts an Ossie Document into a PydanticSemanticManifest. The conversion is inherently lossy: Ossie stores metrics as raw SQL expressions @@ -94,17 +94,17 @@ class OSIToMSIConverter: - anything else → SIMPLE with the raw expression stored in `expr` """ - def __init__(self, dialect: OSIDialect = OSIDialect.ANSI_SQL) -> None: + def __init__(self, dialect: OssieDialect = OssieDialect.ANSI_SQL) -> None: self._dialect = dialect - def convert(self, document: OSIDocument) -> ConverterResult[PydanticSemanticManifest]: + def convert(self, document: OssieDocument) -> ConverterResult[PydanticSemanticManifest]: semantic_models: List[PydanticSemanticModel] = [] metrics: List[PydanticMetric] = [] - for osi_sm in document.semantic_model: - for dataset in osi_sm.datasets: - semantic_models.append(self._convert_dataset(dataset, osi_sm)) - metrics.extend(self._convert_metrics(osi_sm)) + for ossie_sm in document.semantic_model: + for dataset in ossie_sm.datasets: + semantic_models.append(self._convert_dataset(dataset, ossie_sm)) + metrics.extend(self._convert_metrics(ossie_sm)) return ConverterResult( output=PydanticSemanticManifest( @@ -121,10 +121,10 @@ def convert(self, document: OSIDocument) -> ConverterResult[PydanticSemanticMani def _convert_dataset( self, - dataset: OSIDataset, - osi_sm: OSISemanticModel, + dataset: OssieDataset, + ossie_sm: OssieSemanticModel, ) -> PydanticSemanticModel: - key_sets = self._build_key_sets(dataset, osi_sm) + key_sets = self._build_key_sets(dataset, ossie_sm) entities: List[PydanticEntity] = [] dimensions: List[PydanticDimension] = [] @@ -152,14 +152,14 @@ def _convert_dataset( ) @staticmethod - def _build_key_sets(dataset: OSIDataset, osi_sm: OSISemanticModel) -> _KeySets: + def _build_key_sets(dataset: OssieDataset, ossie_sm: OssieSemanticModel) -> _KeySets: """Return a _KeySets with primary, unique, and foreign key column sets for a dataset.""" return _KeySets( primary=set(dataset.primary_key or []), unique={col for keys in (dataset.unique_keys or []) for col in keys}, foreign={ col - for rel in (osi_sm.relationships or []) + for rel in (ossie_sm.relationships or []) if rel.from_dataset == dataset.name for col in rel.from_columns }, @@ -167,7 +167,7 @@ def _build_key_sets(dataset: OSIDataset, osi_sm: OSISemanticModel) -> _KeySets: def _classify_field( self, - field: OSIField, + field: OssieField, expr: str, expr_or_none: Optional[str], primary_key_cols: Set[str], @@ -257,11 +257,11 @@ def _classify_field( # Metric conversion # ------------------------------------------------------------------ - def _convert_metrics(self, osi_sm: OSISemanticModel) -> List[PydanticMetric]: + def _convert_metrics(self, ossie_sm: OssieSemanticModel) -> List[PydanticMetric]: metrics: List[PydanticMetric] = [] - for metric in osi_sm.metrics or []: + for metric in ossie_sm.metrics or []: expr_str = self._get_expression(metric.expression) - metrics.extend(self._convert_metric(metric.name, expr_str, metric.description, osi_sm.datasets)) + metrics.extend(self._convert_metric(metric.name, expr_str, metric.description, ossie_sm.datasets)) return metrics def _convert_metric( @@ -269,7 +269,7 @@ def _convert_metric( name: str, expr_str: str, description: Optional[str], - datasets: List[OSIDataset], + datasets: List[OssieDataset], ) -> List[PydanticMetric]: """Return one or more PydanticMetric objects for the given Ossie expression. @@ -361,7 +361,7 @@ def _convert_metric( def _find_dataset_for_col( raw_expr_str: str, bare_col: str, - datasets: List[OSIDataset], + datasets: List[OssieDataset], ) -> str: """Determine which dataset a column belongs to for `metric_aggregation_params.semantic_model`. @@ -386,12 +386,12 @@ def _find_dataset_for_col( return datasets[0].name if datasets else "" - def _get_expression(self, osi_expr: OSIExpression) -> str: + def _get_expression(self, ossie_expr: OssieExpression) -> str: """Return the expression string for the preferred dialect (fallback: first available).""" - for dialect_expr in osi_expr.dialects: + for dialect_expr in ossie_expr.dialects: if dialect_expr.dialect is self._dialect: return dialect_expr.expression - return osi_expr.dialects[0].expression if osi_expr.dialects else "" + return ossie_expr.dialects[0].expression if ossie_expr.dialects else "" @staticmethod def _parse_source(source: str) -> PydanticNodeRelation: diff --git a/converters/dbt/tests/__snapshots__/test_msi_to_osi.ambr b/converters/dbt/tests/__snapshots__/test_msi_to_ossie.ambr similarity index 100% rename from converters/dbt/tests/__snapshots__/test_msi_to_osi.ambr rename to converters/dbt/tests/__snapshots__/test_msi_to_ossie.ambr diff --git a/converters/dbt/tests/__snapshots__/test_osi_to_msi.ambr b/converters/dbt/tests/__snapshots__/test_ossie_to_msi.ambr similarity index 95% rename from converters/dbt/tests/__snapshots__/test_osi_to_msi.ambr rename to converters/dbt/tests/__snapshots__/test_ossie_to_msi.ambr index e886e0b4..211037fa 100644 --- a/converters/dbt/tests/__snapshots__/test_osi_to_msi.ambr +++ b/converters/dbt/tests/__snapshots__/test_ossie_to_msi.ambr @@ -15,7 +15,7 @@ # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. -# name: TestOSIToMSIRoundTrip.test_osi_to_msi_to_osi_preserves_structure +# name: TestOssieToMSIRoundTrip.test_ossie_to_msi_to_ossie_preserves_structure ''' version: 0.2.0.dev0 dialects: diff --git a/converters/dbt/tests/helpers.py b/converters/dbt/tests/helpers.py index 550a4937..644d53ba 100644 --- a/converters/dbt/tests/helpers.py +++ b/converters/dbt/tests/helpers.py @@ -18,16 +18,16 @@ """Shared test helpers for Ossie converter tests.""" from ossie import ( - OSIDataset, - OSIDialect, - OSIDialectExpression, - OSIDimension, - OSIDocument, - OSIExpression, - OSIField, - OSIMetric, - OSIRelationship, - OSISemanticModel, + OssieDataset, + OssieDialect, + OssieDialectExpression, + OssieDimension, + OssieDocument, + OssieExpression, + OssieField, + OssieMetric, + OssieRelationship, + OssieSemanticModel, ) from metricflow_semantic_interfaces.implementations.elements.dimension import ( PydanticDimension, @@ -157,35 +157,35 @@ def _filter(sql: str) -> PydanticWhereFilterIntersection: # --------------------------------------------------------------------------- -def _osi_expr(expression: str, dialect: OSIDialect = OSIDialect.ANSI_SQL) -> OSIExpression: - return OSIExpression(dialects=[OSIDialectExpression(dialect=dialect, expression=expression)]) +def _ossie_expr(expression: str, dialect: OssieDialect = OssieDialect.ANSI_SQL) -> OssieExpression: + return OssieExpression(dialects=[OssieDialectExpression(dialect=dialect, expression=expression)]) -def _osi_field( +def _ossie_field( name: str, expression: str | None = None, is_time: bool | None = None, description: str | None = None, label: str | None = None, -) -> OSIField: - return OSIField( +) -> OssieField: + return OssieField( name=name, - expression=_osi_expr(expression if expression is not None else name), - dimension=OSIDimension(is_time=is_time) if is_time is not None else None, + expression=_ossie_expr(expression if expression is not None else name), + dimension=OssieDimension(is_time=is_time) if is_time is not None else None, description=description, label=label, ) -def _osi_dataset( +def _ossie_dataset( name: str, source: str = "schema.table", - fields: list[OSIField] | None = None, + fields: list[OssieField] | None = None, primary_key: list[str] | None = None, unique_keys: list[list[str]] | None = None, description: str | None = None, -) -> OSIDataset: - return OSIDataset( +) -> OssieDataset: + return OssieDataset( name=name, source=source, fields=fields, @@ -195,14 +195,14 @@ def _osi_dataset( ) -def _osi_metric(name: str, expression: str, description: str | None = None) -> OSIMetric: - return OSIMetric(name=name, expression=_osi_expr(expression), description=description) +def _ossie_metric(name: str, expression: str, description: str | None = None) -> OssieMetric: + return OssieMetric(name=name, expression=_ossie_expr(expression), description=description) -def _osi_relationship( +def _ossie_relationship( name: str, from_dataset: str, to_dataset: str, from_columns: list[str], to_columns: list[str] -) -> OSIRelationship: - return OSIRelationship( +) -> OssieRelationship: + return OssieRelationship( name=name, from_dataset=from_dataset, to=to_dataset, @@ -211,15 +211,15 @@ def _osi_relationship( ) -def _osi_doc( - datasets: list[OSIDataset] | None = None, - metrics: list[OSIMetric] | None = None, - relationships: list[OSIRelationship] | None = None, +def _ossie_doc( + datasets: list[OssieDataset] | None = None, + metrics: list[OssieMetric] | None = None, + relationships: list[OssieRelationship] | None = None, model_name: str = "test", -) -> OSIDocument: - return OSIDocument( +) -> OssieDocument: + return OssieDocument( semantic_model=[ - OSISemanticModel( + OssieSemanticModel( name=model_name, datasets=datasets or [], metrics=metrics if metrics else None, diff --git a/converters/dbt/tests/test_msi_to_osi.py b/converters/dbt/tests/test_msi_to_ossie.py similarity index 83% rename from converters/dbt/tests/test_msi_to_osi.py rename to converters/dbt/tests/test_msi_to_ossie.py index a0aa9f8a..98102628 100644 --- a/converters/dbt/tests/test_msi_to_osi.py +++ b/converters/dbt/tests/test_msi_to_ossie.py @@ -23,8 +23,8 @@ from ossie_dbt.converter_issues import ConverterIssueType from ossie_dbt.filter_utils import _render_filter_template -from ossie import OSIDialect, OSIDocument -from ossie_dbt.msi_to_osi import MSIToOSIConverter +from ossie import OssieDialect, OssieDocument +from ossie_dbt.msi_to_ossie import MSIToOssieConverter from metricflow_semantic_interfaces.implementations.metric import ( PydanticConversionTypeParams, PydanticCumulativeTypeParams, @@ -61,19 +61,19 @@ # --------------------------------------------------------------------------- -def _fields(result: OSIDocument, dataset_idx: int = 0) -> list: +def _fields(result: OssieDocument, dataset_idx: int = 0) -> list: """Return fields for a dataset, asserting they exist.""" fields = result.semantic_model[0].datasets[dataset_idx].fields assert fields is not None return fields -def _field_expr(result: OSIDocument, field_idx: int = 0) -> str: +def _field_expr(result: OssieDocument, field_idx: int = 0) -> str: """Return the ANSI SQL expression for a field by index.""" return _fields(result)[field_idx].expression.dialects[0].expression -def _osi_metrics(result: OSIDocument) -> list: +def _ossie_metrics(result: OssieDocument) -> list: """Return Ossie metrics for the first semantic model, asserting they exist.""" metrics = result.semantic_model[0].metrics assert metrics is not None @@ -87,7 +87,7 @@ def _osi_metrics(result: OSIDocument) -> list: class TestBasicConversion: def test_empty_manifest_produces_empty_datasets(self) -> None: - result = MSIToOSIConverter().convert(_manifest(), osi_model_name="test").output + result = MSIToOssieConverter().convert(_manifest(), ossie_model_name="test").output assert result.version == "0.2.0.dev0" assert len(result.semantic_model) == 1 @@ -102,7 +102,7 @@ def test_semantic_model_becomes_dataset(self) -> None: description="Order data", node_relation=PydanticNodeRelation(schema_name="analytics", alias="orders_table"), ) - result = MSIToOSIConverter().convert(_manifest(semantic_models=[sm])).output + result = MSIToOssieConverter().convert(_manifest(semantic_models=[sm])).output dataset = result.semantic_model[0].datasets[0] assert dataset.name == "orders" @@ -114,14 +114,14 @@ def test_source_includes_database_when_present(self) -> None: name="orders", node_relation=PydanticNodeRelation(schema_name="analytics", alias="orders_table", database="prod"), ) - result = MSIToOSIConverter().convert(_manifest(semantic_models=[sm])).output + result = MSIToOssieConverter().convert(_manifest(semantic_models=[sm])).output assert result.semantic_model[0].datasets[0].source == "prod.analytics.orders_table" def test_multiple_semantic_models_become_multiple_datasets(self) -> None: sm_a = semantic_model_with_guaranteed_meta(name="orders") sm_b = semantic_model_with_guaranteed_meta(name="users") - result = MSIToOSIConverter().convert(_manifest(semantic_models=[sm_a, sm_b])).output + result = MSIToOssieConverter().convert(_manifest(semantic_models=[sm_a, sm_b])).output names = [ds.name for ds in result.semantic_model[0].datasets] assert names == ["orders", "users"] @@ -133,7 +133,7 @@ def test_categorical_dimension_has_is_time_false(self) -> None: name="orders", dimensions=[_dimension("status", dim_type=DimensionType.CATEGORICAL)], ) - result = MSIToOSIConverter().convert(_manifest(semantic_models=[sm])).output + result = MSIToOssieConverter().convert(_manifest(semantic_models=[sm])).output field = _fields(result)[0] assert field.name == "status" @@ -145,7 +145,7 @@ def test_time_dimension_has_is_time_true(self) -> None: name="orders", dimensions=[_dimension("ds", dim_type=DimensionType.TIME, granularity=TimeGranularity.DAY)], ) - result = MSIToOSIConverter().convert(_manifest(semantic_models=[sm])).output + result = MSIToOssieConverter().convert(_manifest(semantic_models=[sm])).output field = _fields(result)[0] assert field.dimension is not None @@ -156,7 +156,7 @@ def test_dimension_with_expr_uses_expr(self) -> None: name="orders", dimensions=[_dimension("order_date", expr="DATE(created_at)")], ) - result = MSIToOSIConverter().convert(_manifest(semantic_models=[sm])).output + result = MSIToOssieConverter().convert(_manifest(semantic_models=[sm])).output assert _field_expr(result) == "DATE(created_at)" @@ -165,7 +165,7 @@ def test_dimension_without_expr_falls_back_to_name(self) -> None: name="orders", dimensions=[_dimension("status")], ) - result = MSIToOSIConverter().convert(_manifest(semantic_models=[sm])).output + result = MSIToOssieConverter().convert(_manifest(semantic_models=[sm])).output assert _field_expr(result) == "status" @@ -174,7 +174,7 @@ def test_dimension_description_and_label_carried_over(self) -> None: name="orders", dimensions=[_dimension("status", description="Order status", label="Status")], ) - result = MSIToOSIConverter().convert(_manifest(semantic_models=[sm])).output + result = MSIToOssieConverter().convert(_manifest(semantic_models=[sm])).output field = _fields(result)[0] assert field.description == "Order status" @@ -187,7 +187,7 @@ def test_measure_becomes_field_without_dimension_metadata(self) -> None: name="orders", measures=[_measure("revenue", agg=AggregationType.SUM, expr="amount")], ) - result = MSIToOSIConverter().convert(_manifest(semantic_models=[sm])).output + result = MSIToOssieConverter().convert(_manifest(semantic_models=[sm])).output field = _fields(result)[0] assert field.name == "revenue" @@ -199,7 +199,7 @@ def test_measure_without_expr_falls_back_to_name(self) -> None: name="orders", measures=[_measure("num_orders", agg=AggregationType.SUM)], ) - result = MSIToOSIConverter().convert(_manifest(semantic_models=[sm])).output + result = MSIToOssieConverter().convert(_manifest(semantic_models=[sm])).output assert _field_expr(result) == "num_orders" @@ -208,7 +208,7 @@ def test_measure_description_and_label_carried_over(self) -> None: name="orders", measures=[_measure("revenue", description="Total revenue", label="Revenue")], ) - result = MSIToOSIConverter().convert(_manifest(semantic_models=[sm])).output + result = MSIToOssieConverter().convert(_manifest(semantic_models=[sm])).output field = _fields(result)[0] assert field.description == "Total revenue" @@ -221,7 +221,7 @@ def test_entity_becomes_field(self) -> None: name="orders", entities=[_entity("order_id", entity_type=EntityType.PRIMARY)], ) - result = MSIToOSIConverter().convert(_manifest(semantic_models=[sm])).output + result = MSIToOssieConverter().convert(_manifest(semantic_models=[sm])).output field = _fields(result)[0] assert field.name == "order_id" @@ -233,7 +233,7 @@ def test_entity_with_expr_uses_expr(self) -> None: name="orders", entities=[_entity("order_id", entity_type=EntityType.PRIMARY, expr="id")], ) - result = MSIToOSIConverter().convert(_manifest(semantic_models=[sm])).output + result = MSIToOssieConverter().convert(_manifest(semantic_models=[sm])).output field = _fields(result)[0] assert field.name == "order_id" @@ -244,7 +244,7 @@ def test_foreign_entity_also_becomes_field(self) -> None: name="orders", entities=[_entity("user_id", entity_type=EntityType.FOREIGN)], ) - result = MSIToOSIConverter().convert(_manifest(semantic_models=[sm])).output + result = MSIToOssieConverter().convert(_manifest(semantic_models=[sm])).output assert _fields(result)[0].name == "user_id" @@ -271,7 +271,7 @@ def test_key_extraction( name="orders", entities=[_entity(name, entity_type=entity_type, expr=expr)], ) - result = MSIToOSIConverter().convert(_manifest(semantic_models=[sm])).output + result = MSIToOssieConverter().convert(_manifest(semantic_models=[sm])).output dataset = result.semantic_model[0].datasets[0] assert dataset.primary_key == expected_pk @@ -286,7 +286,7 @@ def test_entities_then_dimensions_then_measures(self) -> None: dimensions=[_dimension("status")], measures=[_measure("revenue", expr="amount")], ) - result = MSIToOSIConverter().convert(_manifest(semantic_models=[sm])).output + result = MSIToOssieConverter().convert(_manifest(semantic_models=[sm])).output fields = _fields(result) assert fields[0].name == "order_id" @@ -300,20 +300,20 @@ def test_default_dialect_is_ansi_sql(self) -> None: name="orders", dimensions=[_dimension("status")], ) - result = MSIToOSIConverter().convert(_manifest(semantic_models=[sm])).output + result = MSIToOssieConverter().convert(_manifest(semantic_models=[sm])).output - assert result.dialects == [OSIDialect.ANSI_SQL] - assert _fields(result)[0].expression.dialects[0].dialect == OSIDialect.ANSI_SQL + assert result.dialects == [OssieDialect.ANSI_SQL] + assert _fields(result)[0].expression.dialects[0].dialect == OssieDialect.ANSI_SQL def test_configurable_dialect(self) -> None: sm = semantic_model_with_guaranteed_meta( name="orders", dimensions=[_dimension("status")], ) - result = MSIToOSIConverter(dialect=OSIDialect.SNOWFLAKE).convert(_manifest(semantic_models=[sm])).output + result = MSIToOssieConverter(dialect=OssieDialect.SNOWFLAKE).convert(_manifest(semantic_models=[sm])).output - assert result.dialects == [OSIDialect.SNOWFLAKE] - assert _fields(result)[0].expression.dialects[0].dialect == OSIDialect.SNOWFLAKE + assert result.dialects == [OssieDialect.SNOWFLAKE] + assert _fields(result)[0].expression.dialects[0].dialect == OssieDialect.SNOWFLAKE class TestRelationshipConversion: @@ -326,7 +326,7 @@ def test_shared_entity_name_produces_relationship(self) -> None: name="bookings", entities=[_entity("listing", entity_type=EntityType.FOREIGN, expr="listing_id")], ) - result = MSIToOSIConverter().convert(_manifest(semantic_models=[listings, bookings])).output + result = MSIToOssieConverter().convert(_manifest(semantic_models=[listings, bookings])).output rels = result.semantic_model[0].relationships assert rels is not None @@ -346,7 +346,7 @@ def test_same_type_entities_produce_relationship(self) -> None: name="users_b", entities=[_entity("user", entity_type=EntityType.PRIMARY, expr="uid")], ) - result = MSIToOSIConverter().convert(_manifest(semantic_models=[users_a, users_b])).output + result = MSIToOssieConverter().convert(_manifest(semantic_models=[users_a, users_b])).output rels = result.semantic_model[0].relationships assert rels is not None @@ -359,7 +359,7 @@ def test_single_dataset_with_entity_produces_no_relationship(self) -> None: name="bookings", entities=[_entity("listing", entity_type=EntityType.FOREIGN, expr="listing_id")], ) - result = MSIToOSIConverter().convert(_manifest(semantic_models=[bookings])).output + result = MSIToOssieConverter().convert(_manifest(semantic_models=[bookings])).output assert result.semantic_model[0].relationships is None @@ -371,7 +371,7 @@ def test_same_dataset_entities_excluded(self) -> None: _entity("order", entity_type=EntityType.FOREIGN, expr="order_id"), ], ) - result = MSIToOSIConverter().convert(_manifest(semantic_models=[orders])).output + result = MSIToOssieConverter().convert(_manifest(semantic_models=[orders])).output assert result.semantic_model[0].relationships is None @@ -388,14 +388,14 @@ def test_three_datasets_produce_all_pairs(self, snapshot: SnapshotAssertion) -> name="orders", entities=[_entity("user", entity_type=EntityType.FOREIGN, expr="user_id")], ) - result = MSIToOSIConverter().convert(_manifest(semantic_models=[users_a, users_b, orders])).output + result = MSIToOssieConverter().convert(_manifest(semantic_models=[users_a, users_b, orders])).output rels = result.semantic_model[0].relationships assert rels is not None assert len(rels) == 3 pairs = {(r.from_dataset, r.to) for r in rels} assert pairs == {("users_a", "users_b"), ("orders", "users_a"), ("orders", "users_b")} - assert result.to_osi_yaml() == snapshot + assert result.to_ossie_yaml() == snapshot def test_columns_use_expr_when_present(self) -> None: listings = semantic_model_with_guaranteed_meta( @@ -406,7 +406,7 @@ def test_columns_use_expr_when_present(self) -> None: name="bookings", entities=[_entity("listing", entity_type=EntityType.FOREIGN, expr="fk_lid")], ) - result = MSIToOSIConverter().convert(_manifest(semantic_models=[listings, bookings])).output + result = MSIToOssieConverter().convert(_manifest(semantic_models=[listings, bookings])).output rels = result.semantic_model[0].relationships assert rels is not None @@ -423,7 +423,7 @@ def test_columns_fall_back_to_name_without_expr(self) -> None: name="bookings", entities=[_entity("listing", entity_type=EntityType.FOREIGN)], ) - result = MSIToOSIConverter().convert(_manifest(semantic_models=[listings, bookings])).output + result = MSIToOssieConverter().convert(_manifest(semantic_models=[listings, bookings])).output rels = result.semantic_model[0].relationships assert rels is not None @@ -443,7 +443,7 @@ def test_primary_entity_shorthand_does_not_produce_relationship(self) -> None: name="orders", entities=[_entity("booking", entity_type=EntityType.FOREIGN, expr="booking_id")], ) - result = MSIToOSIConverter().convert(_manifest(semantic_models=[bookings, orders])).output + result = MSIToOssieConverter().convert(_manifest(semantic_models=[bookings, orders])).output assert result.semantic_model[0].relationships is None @@ -456,7 +456,7 @@ def test_relationship_name_format(self) -> None: name="bookings", entities=[_entity("listing", entity_type=EntityType.FOREIGN, expr="listing_id")], ) - result = MSIToOSIConverter().convert(_manifest(semantic_models=[listings, bookings])).output + result = MSIToOssieConverter().convert(_manifest(semantic_models=[listings, bookings])).output rels = result.semantic_model[0].relationships assert rels is not None @@ -471,7 +471,7 @@ def test_natural_entity_excluded(self) -> None: name="orders", entities=[_entity("user", entity_type=EntityType.FOREIGN, expr="user_id")], ) - result = MSIToOSIConverter().convert(_manifest(semantic_models=[users, orders])).output + result = MSIToOssieConverter().convert(_manifest(semantic_models=[users, orders])).output assert result.semantic_model[0].relationships is None @@ -484,7 +484,7 @@ def test_direction_based_on_entity_type_not_manifest_order(self) -> None: name="alpha", entities=[_entity("shared", entity_type=EntityType.FOREIGN, expr="col_a")], ) - result = MSIToOSIConverter().convert(_manifest(semantic_models=[beta, alpha])).output + result = MSIToOssieConverter().convert(_manifest(semantic_models=[beta, alpha])).output rels = result.semantic_model[0].relationships assert rels is not None @@ -501,9 +501,9 @@ def test_simple_metric_resolves_through_measure(self) -> None: measures=[_measure("revenue", agg=AggregationType.SUM, expr="amount")], ) metric = _simple_metric("revenue", measure_name="revenue") - result = MSIToOSIConverter().convert(_manifest(semantic_models=[sm], metrics=[metric])).output + result = MSIToOssieConverter().convert(_manifest(semantic_models=[sm], metrics=[metric])).output - metrics = _osi_metrics(result) + metrics = _ossie_metrics(result) assert len(metrics) == 1 assert metrics[0].name == "revenue" assert metrics[0].expression.dialects[0].expression == "SUM(orders.amount)" @@ -514,9 +514,9 @@ def test_simple_metric_description_carried_over(self) -> None: measures=[_measure("revenue", agg=AggregationType.SUM, expr="amount")], ) metric = _simple_metric("revenue", measure_name="revenue", description="Total revenue") - result = MSIToOSIConverter().convert(_manifest(semantic_models=[sm], metrics=[metric])).output + result = MSIToOssieConverter().convert(_manifest(semantic_models=[sm], metrics=[metric])).output - assert _osi_metrics(result)[0].description == "Total revenue" + assert _ossie_metrics(result)[0].description == "Total revenue" def test_simple_metric_with_metric_aggregation_params(self) -> None: sm = semantic_model_with_guaranteed_meta(name="orders") @@ -538,9 +538,9 @@ def test_simple_metric_with_metric_aggregation_params(self) -> None: metadata=default_meta(), config=None, ) - result = MSIToOSIConverter().convert(_manifest(semantic_models=[sm], metrics=[metric])).output + result = MSIToOssieConverter().convert(_manifest(semantic_models=[sm], metrics=[metric])).output - assert _osi_metrics(result)[0].expression.dialects[0].expression == "AVG(orders.price)" + assert _ossie_metrics(result)[0].expression.dialects[0].expression == "AVG(orders.price)" # --- RATIO --- @@ -567,16 +567,16 @@ def test_ratio_metric_inlines_sub_expressions(self, snapshot: SnapshotAssertion) config=None, ) result = ( - MSIToOSIConverter() + MSIToOssieConverter() .convert(_manifest(semantic_models=[sm], metrics=[revenue_m, order_count_m, arpu])) .output ) - arpu_osi = next(m for m in _osi_metrics(result) if m.name == "arpu") - assert arpu_osi.expression.dialects[0].expression == ( + arpu_ossie = next(m for m in _ossie_metrics(result) if m.name == "arpu") + assert arpu_ossie.expression.dialects[0].expression == ( "(SUM(orders.amount)) / (SUM(CASE WHEN orders.order_id IS NOT NULL THEN 1 ELSE 0 END))" ) - assert result.to_osi_yaml() == snapshot + assert result.to_ossie_yaml() == snapshot # --- DERIVED --- @@ -606,11 +606,11 @@ def test_derived_metric_inlines_sub_expressions(self) -> None: config=None, ) result = ( - MSIToOSIConverter().convert(_manifest(semantic_models=[sm], metrics=[revenue_m, cost_m, profit])).output + MSIToOssieConverter().convert(_manifest(semantic_models=[sm], metrics=[revenue_m, cost_m, profit])).output ) - profit_osi = next(m for m in _osi_metrics(result) if m.name == "profit") - assert profit_osi.expression.dialects[0].expression == "SUM(orders.amount) - SUM(orders.cost_amount)" + profit_ossie = next(m for m in _ossie_metrics(result) if m.name == "profit") + assert profit_ossie.expression.dialects[0].expression == "SUM(orders.amount) - SUM(orders.cost_amount)" def test_derived_metric_uses_alias_for_substitution(self) -> None: sm = semantic_model_with_guaranteed_meta( @@ -638,11 +638,11 @@ def test_derived_metric_uses_alias_for_substitution(self) -> None: config=None, ) result = ( - MSIToOSIConverter().convert(_manifest(semantic_models=[sm], metrics=[revenue_m, cost_m, profit])).output + MSIToOssieConverter().convert(_manifest(semantic_models=[sm], metrics=[revenue_m, cost_m, profit])).output ) - profit_osi = next(m for m in _osi_metrics(result) if m.name == "profit") - assert profit_osi.expression.dialects[0].expression == "SUM(orders.amount) - SUM(orders.cost_amount)" + profit_ossie = next(m for m in _ossie_metrics(result) if m.name == "profit") + assert profit_ossie.expression.dialects[0].expression == "SUM(orders.amount) - SUM(orders.cost_amount)" def test_derived_metric_nested(self, snapshot: SnapshotAssertion) -> None: sm = semantic_model_with_guaranteed_meta( @@ -687,7 +687,7 @@ def test_derived_metric_nested(self, snapshot: SnapshotAssertion) -> None: config=None, ) result = ( - MSIToOSIConverter() + MSIToOssieConverter() .convert( _manifest( semantic_models=[sm], @@ -697,12 +697,12 @@ def test_derived_metric_nested(self, snapshot: SnapshotAssertion) -> None: .output ) - net_osi = next(m for m in _osi_metrics(result) if m.name == "net_profit") + net_ossie = next(m for m in _ossie_metrics(result) if m.name == "net_profit") assert ( - net_osi.expression.dialects[0].expression + net_ossie.expression.dialects[0].expression == "(SUM(orders.amount) - SUM(orders.cost_amount)) - SUM(orders.expense_amount)" ) - assert result.to_osi_yaml() == snapshot + assert result.to_ossie_yaml() == snapshot def test_derived_metric_ref_not_corrupted_by_prefix_match(self) -> None: """Substituting 'revenue' must not corrupt 'revenue_adjusted' when it appears in the same expr.""" @@ -731,13 +731,13 @@ def test_derived_metric_ref_not_corrupted_by_prefix_match(self) -> None: config=None, ) result = ( - MSIToOSIConverter() + MSIToOssieConverter() .convert(_manifest(semantic_models=[sm], metrics=[revenue_m, revenue_adjusted_m, derived])) .output ) - derived_osi = next(m for m in _osi_metrics(result) if m.name == "revenue_delta") - assert derived_osi.expression.dialects[0].expression == "SUM(orders.amount) - SUM(orders.adjusted_amount)" + derived_ossie = next(m for m in _ossie_metrics(result) if m.name == "revenue_delta") + assert derived_ossie.expression.dialects[0].expression == "SUM(orders.amount) - SUM(orders.adjusted_amount)" # --- CUMULATIVE --- @@ -760,9 +760,9 @@ def test_cumulative_metric_uses_base_aggregation(self) -> None: metadata=default_meta(), config=None, ) - result = MSIToOSIConverter().convert(_manifest(semantic_models=[sm], metrics=[cumulative])).output + result = MSIToOssieConverter().convert(_manifest(semantic_models=[sm], metrics=[cumulative])).output - metrics = _osi_metrics(result) + metrics = _ossie_metrics(result) assert len(metrics) == 1 assert metrics[0].name == "cumulative_revenue" assert metrics[0].expression.dialects[0].expression == "SUM(orders.amount)" @@ -794,16 +794,16 @@ def test_cumulative_metric_via_sub_metric_reference(self) -> None: metadata=default_meta(), config=None, ) - result = MSIToOSIConverter().convert(_manifest(semantic_models=[sm], metrics=[base, cumulative])).output + result = MSIToOssieConverter().convert(_manifest(semantic_models=[sm], metrics=[base, cumulative])).output - cumulative_osi = next(m for m in _osi_metrics(result) if m.name == "cumulative_revenue") - assert cumulative_osi.expression.dialects[0].expression == "SUM(orders.amount)" + cumulative_ossie = next(m for m in _ossie_metrics(result) if m.name == "cumulative_revenue") + assert cumulative_ossie.expression.dialects[0].expression == "SUM(orders.amount)" # --- Edge cases --- - def test_no_metrics_produces_no_osi_metrics(self) -> None: + def test_no_metrics_produces_no_ossie_metrics(self) -> None: sm = semantic_model_with_guaranteed_meta(name="orders") - result = MSIToOSIConverter().convert(_manifest(semantic_models=[sm])).output + result = MSIToOssieConverter().convert(_manifest(semantic_models=[sm])).output assert result.semantic_model[0].metrics is None @@ -816,7 +816,7 @@ def test_multiple_metrics_all_converted(self) -> None: ], ) result = ( - MSIToOSIConverter() + MSIToOssieConverter() .convert( _manifest( semantic_models=[sm], @@ -829,7 +829,7 @@ def test_multiple_metrics_all_converted(self) -> None: .output ) - metrics = _osi_metrics(result) + metrics = _ossie_metrics(result) assert len(metrics) == 2 assert {m.name for m in metrics} == {"revenue", "order_count"} @@ -856,7 +856,7 @@ def test_conversion_metric_skipped(self) -> None: metadata=default_meta(), config=None, ) - result = MSIToOSIConverter().convert(_manifest(semantic_models=[sm], metrics=[conversion])).output + result = MSIToOssieConverter().convert(_manifest(semantic_models=[sm], metrics=[conversion])).output assert result.semantic_model[0].metrics is None @@ -885,7 +885,7 @@ def test_conversion_metric_emits_issue(self) -> None: metadata=default_meta(), config=None, ) - result = MSIToOSIConverter().convert(_manifest(semantic_models=[sm], metrics=[conversion])) + result = MSIToOssieConverter().convert(_manifest(semantic_models=[sm], metrics=[conversion])) dropped = [i for i in result.issues if i.issue_type == ConverterIssueType.CONVERSION_METRIC_DROPPED] assert len(dropped) == 1 @@ -908,7 +908,7 @@ def test_private_metric_emits_issue(self) -> None: metadata=default_meta(), config=None, ) - result = MSIToOSIConverter().convert(_manifest(semantic_models=[sm], metrics=[private_metric])) + result = MSIToOssieConverter().convert(_manifest(semantic_models=[sm], metrics=[private_metric])) assert len(result.issues) == 1 assert result.issues[0].issue_type == ConverterIssueType.PRIVATE_METRIC_DROPPED @@ -919,7 +919,7 @@ def test_natural_entity_emits_issue(self) -> None: name="users", entities=[_entity("user", entity_type=EntityType.NATURAL, expr="user_id")], ) - result = MSIToOSIConverter().convert(_manifest(semantic_models=[sm])) + result = MSIToOssieConverter().convert(_manifest(semantic_models=[sm])) assert len(result.issues) == 1 assert result.issues[0].issue_type == ConverterIssueType.NATURAL_ENTITY_DROPPED @@ -945,7 +945,7 @@ def test_cumulative_metric_emits_issue(self) -> None: metadata=default_meta(), config=None, ) - result = MSIToOSIConverter().convert(_manifest(semantic_models=[sm], metrics=[base, cumulative])) + result = MSIToOssieConverter().convert(_manifest(semantic_models=[sm], metrics=[base, cumulative])) cumulative_issues = [i for i in result.issues if i.issue_type == ConverterIssueType.CUMULATIVE_SEMANTICS_LOSS] assert len(cumulative_issues) == 1 @@ -994,10 +994,10 @@ def test_metric_level_filter_inlines_case_when(self) -> None: metadata=default_meta(), config=None, ) - result = MSIToOSIConverter().convert(_manifest(semantic_models=[sm], metrics=[metric])).output + result = MSIToOssieConverter().convert(_manifest(semantic_models=[sm], metrics=[metric])).output assert ( - _osi_metrics(result)[0].expression.dialects[0].expression + _ossie_metrics(result)[0].expression.dialects[0].expression == "SUM(CASE WHEN status = 'paid' THEN orders.amount END)" ) @@ -1017,10 +1017,10 @@ def test_measure_level_filter_inlines_case_when(self) -> None: metadata=default_meta(), config=None, ) - result = MSIToOSIConverter().convert(_manifest(semantic_models=[sm], metrics=[metric])).output + result = MSIToOssieConverter().convert(_manifest(semantic_models=[sm], metrics=[metric])).output assert ( - _osi_metrics(result)[0].expression.dialects[0].expression + _ossie_metrics(result)[0].expression.dialects[0].expression == "SUM(CASE WHEN status = 'paid' THEN orders.amount END)" ) @@ -1040,12 +1040,12 @@ def test_metric_and_measure_filters_combined_with_and(self, snapshot: SnapshotAs metadata=default_meta(), config=None, ) - result = MSIToOSIConverter().convert(_manifest(semantic_models=[sm], metrics=[metric])).output + result = MSIToOssieConverter().convert(_manifest(semantic_models=[sm], metrics=[metric])).output - assert _osi_metrics(result)[0].expression.dialects[0].expression == ( + assert _ossie_metrics(result)[0].expression.dialects[0].expression == ( "SUM(CASE WHEN (status = 'paid') AND (region = 'intl') THEN orders.amount END)" ) - assert result.to_osi_yaml() == snapshot + assert result.to_ossie_yaml() == snapshot def test_jinja_dimension_reference_rendered(self) -> None: sm = semantic_model_with_guaranteed_meta( @@ -1061,10 +1061,10 @@ def test_jinja_dimension_reference_rendered(self) -> None: metadata=default_meta(), config=None, ) - result = MSIToOSIConverter().convert(_manifest(semantic_models=[sm], metrics=[metric])).output + result = MSIToOssieConverter().convert(_manifest(semantic_models=[sm], metrics=[metric])).output assert ( - _osi_metrics(result)[0].expression.dialects[0].expression + _ossie_metrics(result)[0].expression.dialects[0].expression == "SUM(CASE WHEN order__country = 'US' THEN orders.amount END)" ) @@ -1091,12 +1091,12 @@ def test_ratio_metric_filter_propagated_to_both_sides(self) -> None: config=None, ) result = ( - MSIToOSIConverter() + MSIToOssieConverter() .convert(_manifest(semantic_models=[sm], metrics=[revenue_m, order_count_m, arpu])) .output ) - paid_arpu = next(m for m in _osi_metrics(result) if m.name == "paid_arpu") + paid_arpu = next(m for m in _ossie_metrics(result) if m.name == "paid_arpu") assert paid_arpu.expression.dialects[0].expression == ( "(SUM(CASE WHEN status = 'paid' THEN orders.amount END))" " / " @@ -1126,10 +1126,10 @@ def test_derived_metric_filter_propagated_to_sub_expressions(self) -> None: config=None, ) result = ( - MSIToOSIConverter().convert(_manifest(semantic_models=[sm], metrics=[revenue_m, cost_m, profit])).output + MSIToOssieConverter().convert(_manifest(semantic_models=[sm], metrics=[revenue_m, cost_m, profit])).output ) - paid_profit = next(m for m in _osi_metrics(result) if m.name == "paid_profit") + paid_profit = next(m for m in _ossie_metrics(result) if m.name == "paid_profit") assert paid_profit.expression.dialects[0].expression == ( "SUM(CASE WHEN status = 'paid' THEN orders.amount END)" " - " @@ -1142,16 +1142,16 @@ def test_no_filter_produces_plain_expression(self) -> None: measures=[_measure("revenue", agg=AggregationType.SUM, expr="amount")], ) result = ( - MSIToOSIConverter() + MSIToOssieConverter() .convert(_manifest(semantic_models=[sm], metrics=[_simple_metric("revenue", "revenue")])) .output ) - assert _osi_metrics(result)[0].expression.dialects[0].expression == "SUM(orders.amount)" + assert _ossie_metrics(result)[0].expression.dialects[0].expression == "SUM(orders.amount)" -class TestOSIJsonSerialization: - def test_to_osi_json_produces_valid_json(self) -> None: +class TestOssieJsonSerialization: + def test_to_ossie_json_produces_valid_json(self) -> None: sm = semantic_model_with_guaranteed_meta( name="orders", description="Order data", @@ -1159,17 +1159,17 @@ def test_to_osi_json_produces_valid_json(self) -> None: measures=[_measure("revenue", expr="amount")], entities=[_entity("order_id", entity_type=EntityType.PRIMARY)], ) - result = MSIToOSIConverter().convert(_manifest(semantic_models=[sm]), osi_model_name="my_project").output - parsed = json.loads(result.to_osi_json()) + result = MSIToOssieConverter().convert(_manifest(semantic_models=[sm]), ossie_model_name="my_project").output + parsed = json.loads(result.to_ossie_json()) assert parsed["version"] == "0.2.0.dev0" assert len(parsed["semantic_model"]) == 1 assert parsed["semantic_model"][0]["name"] == "my_project" - def test_to_osi_json_excludes_none_fields(self) -> None: + def test_to_ossie_json_excludes_none_fields(self) -> None: sm = semantic_model_with_guaranteed_meta(name="orders") - result = MSIToOSIConverter().convert(_manifest(semantic_models=[sm])).output - parsed = json.loads(result.to_osi_json()) + result = MSIToOssieConverter().convert(_manifest(semantic_models=[sm])).output + parsed = json.loads(result.to_ossie_json()) dataset = parsed["semantic_model"][0]["datasets"][0] assert "primary_key" not in dataset diff --git a/converters/dbt/tests/test_osi_to_msi.py b/converters/dbt/tests/test_ossie_to_msi.py similarity index 59% rename from converters/dbt/tests/test_osi_to_msi.py rename to converters/dbt/tests/test_ossie_to_msi.py index 6f93a167..78900898 100644 --- a/converters/dbt/tests/test_osi_to_msi.py +++ b/converters/dbt/tests/test_ossie_to_msi.py @@ -15,38 +15,38 @@ # specific language governing permissions and limitations # under the License. -"""Tests for OSIToMSIConverter.""" +"""Tests for OssieToMSIConverter.""" import pytest from syrupy.assertion import SnapshotAssertion -from ossie import OSIDataType, OSIDimension -from ossie_dbt.msi_to_osi import MSIToOSIConverter -from ossie_dbt.osi_to_msi import OSIToMSIConverter +from ossie import OssieDataType, OssieDimension +from ossie_dbt.msi_to_ossie import MSIToOssieConverter +from ossie_dbt.ossie_to_msi import OssieToMSIConverter from metricflow_semantic_interfaces.type_enums import ( AggregationType, DimensionType, MetricType, ) from tests.helpers import ( - _osi_dataset, - _osi_doc, - _osi_field, - _osi_metric, - _osi_relationship, + _ossie_dataset, + _ossie_doc, + _ossie_field, + _ossie_metric, + _ossie_relationship, ) -class TestOSIToMSIBasicConversion: +class TestOssieToMSIBasicConversion: def test_empty_document_produces_empty_manifest(self) -> None: - result = OSIToMSIConverter().convert(_osi_doc()).output + result = OssieToMSIConverter().convert(_ossie_doc()).output assert result.semantic_models == [] assert result.metrics == [] def test_single_dataset_becomes_semantic_model(self) -> None: - doc = _osi_doc(datasets=[_osi_dataset("orders", source="analytics.orders_table")]) - result = OSIToMSIConverter().convert(doc).output + doc = _ossie_doc(datasets=[_ossie_dataset("orders", source="analytics.orders_table")]) + result = OssieToMSIConverter().convert(doc).output assert len(result.semantic_models) == 1 sm = result.semantic_models[0] @@ -56,84 +56,84 @@ def test_single_dataset_becomes_semantic_model(self) -> None: assert sm.node_relation.database is None def test_description_carried_over(self) -> None: - doc = _osi_doc(datasets=[_osi_dataset("orders", description="Order data")]) - result = OSIToMSIConverter().convert(doc).output + doc = _ossie_doc(datasets=[_ossie_dataset("orders", description="Order data")]) + result = OssieToMSIConverter().convert(doc).output assert result.semantic_models[0].description == "Order data" def test_source_two_parts(self) -> None: - doc = _osi_doc(datasets=[_osi_dataset("t", source="myschema.mytable")]) - sm = OSIToMSIConverter().convert(doc).output.semantic_models[0] + doc = _ossie_doc(datasets=[_ossie_dataset("t", source="myschema.mytable")]) + sm = OssieToMSIConverter().convert(doc).output.semantic_models[0] assert sm.node_relation.schema_name == "myschema" assert sm.node_relation.alias == "mytable" assert sm.node_relation.database is None def test_source_three_parts(self) -> None: - doc = _osi_doc(datasets=[_osi_dataset("t", source="mydb.myschema.mytable")]) - sm = OSIToMSIConverter().convert(doc).output.semantic_models[0] + doc = _ossie_doc(datasets=[_ossie_dataset("t", source="mydb.myschema.mytable")]) + sm = OssieToMSIConverter().convert(doc).output.semantic_models[0] assert sm.node_relation.database == "mydb" assert sm.node_relation.schema_name == "myschema" assert sm.node_relation.alias == "mytable" def test_source_bare_name(self) -> None: - doc = _osi_doc(datasets=[_osi_dataset("t", source="mytable")]) - sm = OSIToMSIConverter().convert(doc).output.semantic_models[0] + doc = _ossie_doc(datasets=[_ossie_dataset("t", source="mytable")]) + sm = OssieToMSIConverter().convert(doc).output.semantic_models[0] assert sm.node_relation.alias == "mytable" assert sm.node_relation.schema_name == "" def test_multiple_datasets_become_multiple_models(self) -> None: - doc = _osi_doc(datasets=[_osi_dataset("orders"), _osi_dataset("users")]) - result = OSIToMSIConverter().convert(doc).output + doc = _ossie_doc(datasets=[_ossie_dataset("orders"), _ossie_dataset("users")]) + result = OssieToMSIConverter().convert(doc).output names = [sm.name for sm in result.semantic_models] assert names == ["orders", "users"] -class TestOSIToMSIFieldClassification: +class TestOssieToMSIFieldClassification: def test_primary_key_field_becomes_primary_entity(self) -> None: - doc = _osi_doc( + doc = _ossie_doc( datasets=[ - _osi_dataset( + _ossie_dataset( "orders", - fields=[_osi_field("order_id")], + fields=[_ossie_field("order_id")], primary_key=["order_id"], ) ] ) - sm = OSIToMSIConverter().convert(doc).output.semantic_models[0] + sm = OssieToMSIConverter().convert(doc).output.semantic_models[0] assert len(sm.entities) == 1 assert sm.entities[0].name == "order_id" assert sm.entities[0].type.value == "primary" def test_unique_key_field_becomes_unique_entity(self) -> None: - doc = _osi_doc( + doc = _ossie_doc( datasets=[ - _osi_dataset( + _ossie_dataset( "users", - fields=[_osi_field("email")], + fields=[_ossie_field("email")], unique_keys=[["email"]], ) ] ) - sm = OSIToMSIConverter().convert(doc).output.semantic_models[0] + sm = OssieToMSIConverter().convert(doc).output.semantic_models[0] assert len(sm.entities) == 1 assert sm.entities[0].name == "email" assert sm.entities[0].type.value == "unique" def test_relationship_from_column_becomes_foreign_entity(self) -> None: - doc = _osi_doc( + doc = _ossie_doc( datasets=[ - _osi_dataset("orders", fields=[_osi_field("user_id")]), - _osi_dataset("users", primary_key=["user_id"]), + _ossie_dataset("orders", fields=[_ossie_field("user_id")]), + _ossie_dataset("users", primary_key=["user_id"]), ], - relationships=[_osi_relationship("r", "orders", "users", ["user_id"], ["user_id"])], + relationships=[_ossie_relationship("r", "orders", "users", ["user_id"], ["user_id"])], ) - orders_sm = OSIToMSIConverter().convert(doc).output.semantic_models[0] + orders_sm = OssieToMSIConverter().convert(doc).output.semantic_models[0] assert len(orders_sm.entities) == 1 assert orders_sm.entities[0].name == "user_id" @@ -150,8 +150,8 @@ def test_relationship_from_column_becomes_foreign_entity(self) -> None: def test_field_becomes_dimension_by_is_time( self, field_name: str, is_time: bool | None, expected_type: DimensionType ) -> None: - doc = _osi_doc(datasets=[_osi_dataset("orders", fields=[_osi_field(field_name, is_time=is_time)])]) - sm = OSIToMSIConverter().convert(doc).output.semantic_models[0] + doc = _ossie_doc(datasets=[_ossie_dataset("orders", fields=[_ossie_field(field_name, is_time=is_time)])]) + sm = OssieToMSIConverter().convert(doc).output.semantic_models[0] assert len(sm.dimensions) == 1 assert sm.dimensions[0].name == field_name @@ -160,89 +160,89 @@ def test_field_becomes_dimension_by_is_time( @pytest.mark.parametrize( ("dimension", "datatype", "expected_type"), [ - (OSIDimension(), OSIDataType.DATE, DimensionType.TIME), - (OSIDimension(is_time=False), OSIDataType.DATE_TIME_TZ, DimensionType.CATEGORICAL), - (None, OSIDataType.DATE, DimensionType.CATEGORICAL), + (OssieDimension(), OssieDataType.DATE, DimensionType.TIME), + (OssieDimension(is_time=False), OssieDataType.DATE_TIME_TZ, DimensionType.CATEGORICAL), + (None, OssieDataType.DATE, DimensionType.CATEGORICAL), ], ) def test_field_becomes_dimension_by_effective_time_role( self, - dimension: OSIDimension | None, - datatype: OSIDataType, + dimension: OssieDimension | None, + datatype: OssieDataType, expected_type: DimensionType, ) -> None: - field = _osi_field("occurred_at").model_copy( + field = _ossie_field("occurred_at").model_copy( update={"dimension": dimension, "datatype": datatype} ) - doc = _osi_doc(datasets=[_osi_dataset("events", fields=[field])]) + doc = _ossie_doc(datasets=[_ossie_dataset("events", fields=[field])]) - sm = OSIToMSIConverter().convert(doc).output.semantic_models[0] + sm = OssieToMSIConverter().convert(doc).output.semantic_models[0] assert sm.dimensions[0].type == expected_type def test_field_referenced_in_metric_stays_as_dimension(self) -> None: """Fields referenced in metric expressions are no longer promoted to measures.""" - doc = _osi_doc( - datasets=[_osi_dataset("orders", fields=[_osi_field("amount")])], - metrics=[_osi_metric("revenue", "SUM(amount)")], + doc = _ossie_doc( + datasets=[_ossie_dataset("orders", fields=[_ossie_field("amount")])], + metrics=[_ossie_metric("revenue", "SUM(amount)")], ) - sm = OSIToMSIConverter().convert(doc).output.semantic_models[0] + sm = OssieToMSIConverter().convert(doc).output.semantic_models[0] assert len(sm.measures) == 0 assert len(sm.dimensions) == 1 assert sm.dimensions[0].name == "amount" def test_expr_different_from_name_is_preserved(self) -> None: - doc = _osi_doc( + doc = _ossie_doc( datasets=[ - _osi_dataset( + _ossie_dataset( "orders", - fields=[_osi_field("order_id", expression="id")], + fields=[_ossie_field("order_id", expression="id")], primary_key=["order_id"], ) ] ) - sm = OSIToMSIConverter().convert(doc).output.semantic_models[0] + sm = OssieToMSIConverter().convert(doc).output.semantic_models[0] assert sm.entities[0].expr == "id" def test_expr_same_as_name_is_stored_as_none(self) -> None: - doc = _osi_doc( + doc = _ossie_doc( datasets=[ - _osi_dataset( + _ossie_dataset( "orders", - fields=[_osi_field("order_id")], + fields=[_ossie_field("order_id")], primary_key=["order_id"], ) ] ) - sm = OSIToMSIConverter().convert(doc).output.semantic_models[0] + sm = OssieToMSIConverter().convert(doc).output.semantic_models[0] assert sm.entities[0].expr is None def test_description_and_label_carried_over_to_dimension(self) -> None: - doc = _osi_doc( + doc = _ossie_doc( datasets=[ - _osi_dataset( + _ossie_dataset( "orders", - fields=[_osi_field("status", description="Order status", label="Status")], + fields=[_ossie_field("status", description="Order status", label="Status")], ) ] ) - sm = OSIToMSIConverter().convert(doc).output.semantic_models[0] + sm = OssieToMSIConverter().convert(doc).output.semantic_models[0] dim = sm.dimensions[0] assert dim.description == "Order status" assert dim.label == "Status" -class TestOSIToMSIMetricConversion: +class TestOssieToMSIMetricConversion: def test_sum_expression_produces_simple_metric(self) -> None: - doc = _osi_doc( - datasets=[_osi_dataset("orders", fields=[_osi_field("amount")])], - metrics=[_osi_metric("revenue", "SUM(amount)")], + doc = _ossie_doc( + datasets=[_ossie_dataset("orders", fields=[_ossie_field("amount")])], + metrics=[_ossie_metric("revenue", "SUM(amount)")], ) - result = OSIToMSIConverter().convert(doc).output + result = OssieToMSIConverter().convert(doc).output assert len(result.metrics) == 1 m = result.metrics[0] @@ -255,11 +255,11 @@ def test_sum_expression_produces_simple_metric(self) -> None: assert m.type_params.metric_aggregation_params.semantic_model == "orders" def test_count_distinct_expression(self) -> None: - doc = _osi_doc( - datasets=[_osi_dataset("orders", fields=[_osi_field("user_id")])], - metrics=[_osi_metric("unique_users", "COUNT(DISTINCT user_id)")], + doc = _ossie_doc( + datasets=[_ossie_dataset("orders", fields=[_ossie_field("user_id")])], + metrics=[_ossie_metric("unique_users", "COUNT(DISTINCT user_id)")], ) - result = OSIToMSIConverter().convert(doc).output + result = OssieToMSIConverter().convert(doc).output m = result.metrics[0] assert m.type_params.measure is None @@ -270,16 +270,16 @@ def test_count_distinct_expression(self) -> None: assert len(sm.measures) == 0 def test_ratio_expression_produces_ratio_metric(self) -> None: - doc = _osi_doc( + doc = _ossie_doc( datasets=[ - _osi_dataset( + _ossie_dataset( "orders", - fields=[_osi_field("amount"), _osi_field("order_id")], + fields=[_ossie_field("amount"), _ossie_field("order_id")], ) ], - metrics=[_osi_metric("arpu", "(SUM(amount)) / (COUNT(order_id))")], + metrics=[_ossie_metric("arpu", "(SUM(amount)) / (COUNT(order_id))")], ) - result = OSIToMSIConverter().convert(doc).output + result = OssieToMSIConverter().convert(doc).output ratio = next(m for m in result.metrics if m.type == MetricType.RATIO) assert ratio.name == "arpu" @@ -289,11 +289,11 @@ def test_ratio_expression_produces_ratio_metric(self) -> None: assert ratio.type_params.denominator.name == "arpu__denominator" def test_ratio_sub_metrics_are_simple(self) -> None: - doc = _osi_doc( - datasets=[_osi_dataset("orders", fields=[_osi_field("amount"), _osi_field("cnt")])], - metrics=[_osi_metric("ratio", "(SUM(amount)) / (COUNT(cnt))")], + doc = _ossie_doc( + datasets=[_ossie_dataset("orders", fields=[_ossie_field("amount"), _ossie_field("cnt")])], + metrics=[_ossie_metric("ratio", "(SUM(amount)) / (COUNT(cnt))")], ) - result = OSIToMSIConverter().convert(doc).output + result = OssieToMSIConverter().convert(doc).output simple_metrics = [m for m in result.metrics if m.type == MetricType.SIMPLE] assert len(simple_metrics) == 2 @@ -301,11 +301,11 @@ def test_ratio_sub_metrics_are_simple(self) -> None: assert names == {"ratio__numerator", "ratio__denominator"} def test_complex_expression_falls_back_to_simple_with_raw_expr(self) -> None: - doc = _osi_doc( - datasets=[_osi_dataset("orders")], - metrics=[_osi_metric("complex", "SUM(a) + SUM(b)")], + doc = _ossie_doc( + datasets=[_ossie_dataset("orders")], + metrics=[_ossie_metric("complex", "SUM(a) + SUM(b)")], ) - result = OSIToMSIConverter().convert(doc).output + result = OssieToMSIConverter().convert(doc).output assert len(result.metrics) == 1 m = result.metrics[0] @@ -315,27 +315,27 @@ def test_complex_expression_falls_back_to_simple_with_raw_expr(self) -> None: assert m.type_params.expr == "SUM(a) + SUM(b)" def test_metric_description_carried_over(self) -> None: - doc = _osi_doc( - datasets=[_osi_dataset("orders", fields=[_osi_field("amount")])], - metrics=[_osi_metric("revenue", "SUM(amount)", description="Total revenue")], + doc = _ossie_doc( + datasets=[_ossie_dataset("orders", fields=[_ossie_field("amount")])], + metrics=[_ossie_metric("revenue", "SUM(amount)", description="Total revenue")], ) - result = OSIToMSIConverter().convert(doc).output + result = OssieToMSIConverter().convert(doc).output assert result.metrics[0].description == "Total revenue" def test_no_metrics_produces_empty_list(self) -> None: - doc = _osi_doc(datasets=[_osi_dataset("orders")]) - result = OSIToMSIConverter().convert(doc).output + doc = _ossie_doc(datasets=[_ossie_dataset("orders")]) + result = OssieToMSIConverter().convert(doc).output assert result.metrics == [] def test_dataset_qualified_column_reference(self) -> None: """A metric referencing 'dataset.col' should resolve the semantic_model to that dataset.""" - doc = _osi_doc( - datasets=[_osi_dataset("orders", fields=[_osi_field("amount")])], - metrics=[_osi_metric("revenue", "SUM(orders.amount)")], + doc = _ossie_doc( + datasets=[_ossie_dataset("orders", fields=[_ossie_field("amount")])], + metrics=[_ossie_metric("revenue", "SUM(orders.amount)")], ) - result = OSIToMSIConverter().convert(doc).output + result = OssieToMSIConverter().convert(doc).output m = result.metrics[0] assert m.type_params.metric_aggregation_params is not None @@ -343,11 +343,11 @@ def test_dataset_qualified_column_reference(self) -> None: assert m.type_params.expr == "amount" def test_percentile_cont_0_5_produces_median(self) -> None: - doc = _osi_doc( - datasets=[_osi_dataset("orders", fields=[_osi_field("amount")])], - metrics=[_osi_metric("median_amount", "PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY amount)")], + doc = _ossie_doc( + datasets=[_ossie_dataset("orders", fields=[_ossie_field("amount")])], + metrics=[_ossie_metric("median_amount", "PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY amount)")], ) - result = OSIToMSIConverter().convert(doc).output + result = OssieToMSIConverter().convert(doc).output m = result.metrics[0] assert m.type_params.metric_aggregation_params is not None @@ -356,11 +356,11 @@ def test_percentile_cont_0_5_produces_median(self) -> None: assert m.type_params.expr == "amount" def test_percentile_cont_non_median_carries_percentile_param(self) -> None: - doc = _osi_doc( - datasets=[_osi_dataset("orders", fields=[_osi_field("amount")])], - metrics=[_osi_metric("p95_amount", "PERCENTILE_CONT(0.95) WITHIN GROUP (ORDER BY amount)")], + doc = _ossie_doc( + datasets=[_ossie_dataset("orders", fields=[_ossie_field("amount")])], + metrics=[_ossie_metric("p95_amount", "PERCENTILE_CONT(0.95) WITHIN GROUP (ORDER BY amount)")], ) - result = OSIToMSIConverter().convert(doc).output + result = OssieToMSIConverter().convert(doc).output m = result.metrics[0] assert m.type_params.metric_aggregation_params is not None @@ -370,32 +370,32 @@ def test_percentile_cont_non_median_carries_percentile_param(self) -> None: assert m.type_params.expr == "amount" -class TestOSIToMSIRoundTrip: - def test_osi_to_msi_to_osi_preserves_structure(self, snapshot: SnapshotAssertion) -> None: +class TestOssieToMSIRoundTrip: + def test_ossie_to_msi_to_ossie_preserves_structure(self, snapshot: SnapshotAssertion) -> None: """Ossie → MSI → Ossie preserves dataset names, fields, and metric expressions.""" - original = _osi_doc( + original = _ossie_doc( datasets=[ - _osi_dataset( + _ossie_dataset( "orders", source="analytics.orders", fields=[ - _osi_field("order_id"), - _osi_field("status"), - _osi_field("created_at", is_time=True), - _osi_field("amount"), + _ossie_field("order_id"), + _ossie_field("status"), + _ossie_field("created_at", is_time=True), + _ossie_field("amount"), ], primary_key=["order_id"], ) ], - metrics=[_osi_metric("revenue", "SUM(orders.amount)")], + metrics=[_ossie_metric("revenue", "SUM(orders.amount)")], ) - msi = OSIToMSIConverter().convert(original).output + msi = OssieToMSIConverter().convert(original).output assert msi.semantic_models[0].measures == [] - osi_doc = MSIToOSIConverter().convert(msi).output + ossie_doc = MSIToOssieConverter().convert(msi).output - dataset = osi_doc.semantic_model[0].datasets[0] + dataset = ossie_doc.semantic_model[0].datasets[0] assert dataset.name == "orders" field_names = {f.name for f in dataset.fields or []} @@ -404,8 +404,8 @@ def test_osi_to_msi_to_osi_preserves_structure(self, snapshot: SnapshotAssertion assert "created_at" in field_names assert "amount" in field_names - metrics = osi_doc.semantic_model[0].metrics or [] + metrics = ossie_doc.semantic_model[0].metrics or [] assert len(metrics) == 1 assert metrics[0].name == "revenue" assert metrics[0].expression.dialects[0].expression == "SUM(orders.amount)" - assert osi_doc.to_osi_yaml() == snapshot + assert ossie_doc.to_ossie_yaml() == snapshot diff --git a/converters/gooddata/README.md b/converters/gooddata/README.md index f2388667..9bdfde7f 100644 --- a/converters/gooddata/README.md +++ b/converters/gooddata/README.md @@ -35,20 +35,20 @@ semantic model specification. ```python import json import yaml -from ossie_gooddata import gooddata_to_osi, osi_to_gooddata +from ossie_gooddata import gooddata_to_ossie, ossie_to_gooddata from ossie_gooddata.models import gd_model_from_dict, gd_model_to_dict # GoodData → Apache Ossie with open("gooddata_ldm.json") as f: gd_model = gd_model_from_dict(json.load(f)) -osi_model = gooddata_to_osi(gd_model, model_name="my_model") -with open("osi_model.yaml", "w") as f: - yaml.dump(osi_model, f, default_flow_style=False) +ossie_model = gooddata_to_ossie(gd_model, model_name="my_model") +with open("ossie_model.yaml", "w") as f: + yaml.dump(ossie_model, f, default_flow_style=False) # Apache Ossie → GoodData -with open("osi_model.yaml") as f: - osi_data = yaml.safe_load(f) -gd_model = osi_to_gooddata(osi_data, data_source_id="my_datasource") +with open("ossie_model.yaml") as f: + ossie_data = yaml.safe_load(f) +gd_model = ossie_to_gooddata(ossie_data, data_source_id="my_datasource") with open("gooddata_ldm.json", "w") as f: json.dump(gd_model_to_dict(gd_model), f, indent=2) ``` diff --git a/converters/gooddata/pyproject.toml b/converters/gooddata/pyproject.toml index 98a56e7b..b0872eba 100644 --- a/converters/gooddata/pyproject.toml +++ b/converters/gooddata/pyproject.toml @@ -23,7 +23,7 @@ authors = [{name = "GoodData Corporation", email = "support@gooddata.com"}] readme = "README.md" requires-python = ">=3.12" classifiers = [ - "License :: OSI Approved :: Apache Software License", + "License :: Ossie Approved :: Apache Software License", "Programming Language :: Python :: 3", ] dependencies = [ diff --git a/converters/gooddata/src/ossie_gooddata/__init__.py b/converters/gooddata/src/ossie_gooddata/__init__.py index 6074ef57..8eea15bc 100644 --- a/converters/gooddata/src/ossie_gooddata/__init__.py +++ b/converters/gooddata/src/ossie_gooddata/__init__.py @@ -17,7 +17,7 @@ """Bidirectional converter between GoodData LDM and Ossie semantic model.""" -from ossie_gooddata.gooddata_to_osi import gooddata_to_osi -from ossie_gooddata.osi_to_gooddata import osi_to_gooddata +from ossie_gooddata.gooddata_to_ossie import gooddata_to_ossie +from ossie_gooddata.ossie_to_gooddata import ossie_to_gooddata -__all__ = ["gooddata_to_osi", "osi_to_gooddata"] +__all__ = ["gooddata_to_ossie", "ossie_to_gooddata"] diff --git a/converters/gooddata/src/ossie_gooddata/gooddata_to_osi.py b/converters/gooddata/src/ossie_gooddata/gooddata_to_ossie.py similarity index 84% rename from converters/gooddata/src/ossie_gooddata/gooddata_to_osi.py rename to converters/gooddata/src/ossie_gooddata/gooddata_to_ossie.py index c658d292..05a51981 100644 --- a/converters/gooddata/src/ossie_gooddata/gooddata_to_osi.py +++ b/converters/gooddata/src/ossie_gooddata/gooddata_to_ossie.py @@ -33,10 +33,10 @@ GdReference, ) -OSI_VERSION = "0.2.0.dev0" +OSSIE_VERSION = "0.2.0.dev0" -def gooddata_to_osi( +def gooddata_to_ossie( model: GdDeclarativeModel, model_name: str = "gooddata_model", model_description: str = "", @@ -53,8 +53,8 @@ def gooddata_to_osi( Returns: Ossie semantic model as a dict (ready to serialize to YAML). """ - osi_datasets = [] - osi_relationships = [] + ossie_datasets = [] + ossie_relationships = [] # Map dataset id -> {attribute id -> source column}. Reference targets point # at a grain attribute id; Ossie relationships need the physical column name. @@ -64,22 +64,22 @@ def gooddata_to_osi( # Convert regular datasets for ds in model.ldm.datasets: - osi_ds, rels = _convert_dataset(ds, attr_source_col_map) - osi_datasets.append(osi_ds) - osi_relationships.extend(rels) + ossie_ds, rels = _convert_dataset(ds, attr_source_col_map) + ossie_datasets.append(ossie_ds) + ossie_relationships.extend(rels) # Convert date instances to datasets for di in model.ldm.date_instances: - osi_datasets.append(_convert_date_instance(di)) + ossie_datasets.append(_convert_date_instance(di)) semantic_model: dict[str, Any] = { "name": model_name, - "datasets": osi_datasets, + "datasets": ossie_datasets, } if model_description: semantic_model["description"] = model_description - if osi_relationships: - semantic_model["relationships"] = osi_relationships + if ossie_relationships: + semantic_model["relationships"] = ossie_relationships # Model-level custom_extensions for GoodData workspace metadata extensions = [] @@ -94,7 +94,7 @@ def gooddata_to_osi( semantic_model["custom_extensions"] = extensions return { - "version": OSI_VERSION, + "version": OSSIE_VERSION, "semantic_model": [semantic_model], } @@ -116,18 +116,18 @@ def _convert_dataset( for fact in ds.facts: fields.append(_convert_fact(fact, ds.id)) - osi_ds: dict[str, Any] = { + ossie_ds: dict[str, Any] = { "name": ds.id, "source": source, } if primary_key: - osi_ds["primary_key"] = primary_key + ossie_ds["primary_key"] = primary_key if ds.description: - osi_ds["description"] = ds.description + ossie_ds["description"] = ds.description if ds.title != ds.id: - osi_ds["ai_context"] = {"synonyms": [ds.title]} + ossie_ds["ai_context"] = {"synonyms": [ds.title]} if fields: - osi_ds["fields"] = fields + ossie_ds["fields"] = fields # Convert references to Ossie relationships relationships = [] @@ -135,36 +135,36 @@ def _convert_dataset( rel = _convert_reference(ds.id, ref, attr_source_col_map) relationships.append(rel) - return osi_ds, relationships + return ossie_ds, relationships def _convert_date_instance(di: GdDateInstance) -> dict[str, Any]: """Convert a GoodData date instance to an Ossie dataset.""" - osi_ds: dict[str, Any] = { + ossie_ds: dict[str, Any] = { "name": di.id, "source": di.id, } if di.description: - osi_ds["description"] = di.description + ossie_ds["description"] = di.description if di.title != di.id: - osi_ds["ai_context"] = {"synonyms": [di.title]} + ossie_ds["ai_context"] = {"synonyms": [di.title]} # Store date-specific metadata in custom_extensions ext_data: dict[str, Any] = { "date_dimension": True, "granularities": di.granularities, } - osi_ds["custom_extensions"] = [ + ossie_ds["custom_extensions"] = [ {"vendor_name": "GOODDATA", "data": json.dumps(ext_data)}, ] - return osi_ds + return ossie_ds def _convert_attribute(attr: GdAttribute, dataset_id: str) -> dict[str, Any]: """Convert a GoodData attribute to an Ossie field.""" datatype = gooddata_to_ossie_datatype(attr.source_column_data_type) - osi_field: dict[str, Any] = { + ossie_field: dict[str, Any] = { "name": attr.source_column, "expression": { "dialects": [ @@ -175,26 +175,26 @@ def _convert_attribute(attr: GdAttribute, dataset_id: str) -> dict[str, Any]: "dimension": {"is_time": False}, } if datatype is not None: - osi_field["datatype"] = datatype + ossie_field["datatype"] = datatype if attr.description: - osi_field["description"] = attr.description + ossie_field["description"] = attr.description if attr.title != attr.source_column: - osi_field.setdefault("ai_context", {})["synonyms"] = [attr.title] + ossie_field.setdefault("ai_context", {})["synonyms"] = [attr.title] # Store GoodData-specific attribute metadata in custom_extensions ext = _build_attribute_extension(attr) if datatype == "Opaque": ext["source_column_data_type"] = attr.source_column_data_type if ext: - osi_field["custom_extensions"] = [{"vendor_name": "GOODDATA", "data": json.dumps(ext)}] + ossie_field["custom_extensions"] = [{"vendor_name": "GOODDATA", "data": json.dumps(ext)}] - return osi_field + return ossie_field def _convert_fact(fact: GdFact, dataset_id: str) -> dict[str, Any]: """Convert a GoodData fact to an Ossie field.""" datatype = gooddata_to_ossie_datatype(fact.source_column_data_type) - osi_field: dict[str, Any] = { + ossie_field: dict[str, Any] = { "name": fact.source_column, "expression": { "dialects": [ @@ -204,20 +204,20 @@ def _convert_fact(fact: GdFact, dataset_id: str) -> dict[str, Any]: }, } if datatype is not None: - osi_field["datatype"] = datatype + ossie_field["datatype"] = datatype if fact.description: - osi_field["description"] = fact.description + ossie_field["description"] = fact.description if fact.title != fact.source_column: - osi_field.setdefault("ai_context", {})["synonyms"] = [fact.title] + ossie_field.setdefault("ai_context", {})["synonyms"] = [fact.title] if datatype == "Opaque": - osi_field["custom_extensions"] = [ + ossie_field["custom_extensions"] = [ { "vendor_name": "GOODDATA", "data": json.dumps({"source_column_data_type": fact.source_column_data_type}), } ] - return osi_field + return ossie_field def _convert_reference( diff --git a/converters/gooddata/src/ossie_gooddata/osi_to_gooddata.py b/converters/gooddata/src/ossie_gooddata/ossie_to_gooddata.py similarity index 98% rename from converters/gooddata/src/ossie_gooddata/osi_to_gooddata.py rename to converters/gooddata/src/ossie_gooddata/ossie_to_gooddata.py index a8dbac1c..21a4a910 100644 --- a/converters/gooddata/src/ossie_gooddata/osi_to_gooddata.py +++ b/converters/gooddata/src/ossie_gooddata/ossie_to_gooddata.py @@ -46,14 +46,14 @@ _MAQL_FACT_RE = re.compile(r"\{fact/([^.]+)\.([^}]+)\}") -def osi_to_gooddata( - osi_model: dict[str, Any], +def ossie_to_gooddata( + ossie_model: dict[str, Any], data_source_id: str = "default", ) -> GdDeclarativeModel: """Convert an Ossie semantic model dict to a GoodData declarative model. Args: - osi_model: Parsed Ossie YAML as a dict. + ossie_model: Parsed Ossie YAML as a dict. data_source_id: GoodData data source ID to use for table references. Returns: @@ -62,7 +62,7 @@ def osi_to_gooddata( datasets: list[GdDataset] = [] date_instances: list[GdDateInstance] = [] - for sm in osi_model.get("semantic_model", []): + for sm in ossie_model.get("semantic_model", []): relationship_map = _build_relationship_map(sm) # Pre-pass: for each Ossie dataset, record whether it is a date instance # and map its physical source columns to the attribute ids that will @@ -70,7 +70,7 @@ def osi_to_gooddata( target_info = _build_target_info(sm) for ds in sm.get("datasets", []): - gd_ds, date_inst = _convert_osi_dataset( + gd_ds, date_inst = _convert_ossie_dataset( ds, relationship_map, target_info, data_source_id, ) if date_inst: @@ -123,7 +123,7 @@ def _build_relationship_map( return rel_map -def _convert_osi_dataset( +def _convert_ossie_dataset( ds: dict[str, Any], relationship_map: dict[str, list[dict[str, Any]]], target_info: dict[str, dict[str, Any]], diff --git a/converters/gooddata/tests/conftest.py b/converters/gooddata/tests/conftest.py index bb8c8c82..9e03bd82 100644 --- a/converters/gooddata/tests/conftest.py +++ b/converters/gooddata/tests/conftest.py @@ -44,7 +44,7 @@ def gooddata_tpcds_model(gooddata_tpcds_dict: dict) -> GdDeclarativeModel: @pytest.fixture() -def osi_tpcds_dict() -> dict: +def ossie_tpcds_dict() -> dict: """Load the Ossie TPC-DS YAML fixture.""" - with open(FIXTURES_DIR / "osi_tpcds.yaml") as f: + with open(FIXTURES_DIR / "ossie_tpcds.yaml") as f: return yaml.safe_load(f) diff --git a/converters/gooddata/tests/fixtures/osi_tpcds.yaml b/converters/gooddata/tests/fixtures/ossie_tpcds.yaml similarity index 100% rename from converters/gooddata/tests/fixtures/osi_tpcds.yaml rename to converters/gooddata/tests/fixtures/ossie_tpcds.yaml diff --git a/converters/gooddata/tests/test_gooddata_to_osi.py b/converters/gooddata/tests/test_gooddata_to_ossie.py similarity index 87% rename from converters/gooddata/tests/test_gooddata_to_osi.py rename to converters/gooddata/tests/test_gooddata_to_ossie.py index f330ba33..a714713f 100644 --- a/converters/gooddata/tests/test_gooddata_to_osi.py +++ b/converters/gooddata/tests/test_gooddata_to_ossie.py @@ -23,10 +23,10 @@ import pytest -from ossie_gooddata.gooddata_to_osi import ( +from ossie_gooddata.gooddata_to_ossie import ( _convert_attribute, _convert_fact, - gooddata_to_osi, + gooddata_to_ossie, ) from ossie_gooddata.models import GdAttribute, GdDeclarativeModel, GdFact @@ -58,13 +58,13 @@ def test_native_source_types_become_ossie_datatypes(gooddata_type: str, ossie_ty source_column_data_type=gooddata_type, ) - osi_attribute = _convert_attribute(attribute, "orders") - osi_fact = _convert_fact(fact, "orders") + ossie_attribute = _convert_attribute(attribute, "orders") + ossie_fact = _convert_fact(fact, "orders") - assert osi_attribute["datatype"] == ossie_type - assert osi_attribute["dimension"]["is_time"] is False - assert osi_fact["datatype"] == ossie_type - assert "dimension" not in osi_fact + assert ossie_attribute["datatype"] == ossie_type + assert ossie_attribute["dimension"]["is_time"] is False + assert ossie_fact["datatype"] == ossie_type + assert "dimension" not in ossie_fact @pytest.mark.parametrize( @@ -94,10 +94,10 @@ def test_native_source_types_become_ossie_datatypes(gooddata_type: str, ossie_ty ) def test_unknown_source_type_becomes_opaque_and_is_preserved(source_type: str, converter, model): """Verify unknown GoodData types survive in an exact vendor extension.""" - osi_field = converter(model, "orders") + ossie_field = converter(model, "orders") - assert osi_field["datatype"] == "Opaque" - extension = json.loads(osi_field["custom_extensions"][0]["data"]) + assert ossie_field["datatype"] == "Opaque" + extension = json.loads(ossie_field["custom_extensions"][0]["data"]) assert extension["source_column_data_type"] == source_type @@ -116,7 +116,7 @@ def test_missing_or_empty_source_type_is_omitted(source_type: str | None): def test_basic_conversion(gooddata_tpcds_model: GdDeclarativeModel): """Verify basic structure of GoodData → Ossie conversion.""" - result = gooddata_to_osi(gooddata_tpcds_model, model_name="tpcds_test") + result = gooddata_to_ossie(gooddata_tpcds_model, model_name="tpcds_test") assert result["version"] == "0.2.0.dev0" assert len(result["semantic_model"]) == 1 @@ -127,7 +127,7 @@ def test_basic_conversion(gooddata_tpcds_model: GdDeclarativeModel): def test_datasets_converted(gooddata_tpcds_model: GdDeclarativeModel): """Verify all datasets (regular + date instances) are converted.""" - result = gooddata_to_osi(gooddata_tpcds_model) + result = gooddata_to_ossie(gooddata_tpcds_model) sm = result["semantic_model"][0] # 4 regular datasets + 1 date instance = 5 datasets @@ -143,7 +143,7 @@ def test_datasets_converted(gooddata_tpcds_model: GdDeclarativeModel): def test_dataset_source(gooddata_tpcds_model: GdDeclarativeModel): """Verify source is built from dataSourceTableId.""" - result = gooddata_to_osi(gooddata_tpcds_model) + result = gooddata_to_ossie(gooddata_tpcds_model) sm = result["semantic_model"][0] store_sales = next(ds for ds in sm["datasets"] if ds["name"] == "store_sales") @@ -152,7 +152,7 @@ def test_dataset_source(gooddata_tpcds_model: GdDeclarativeModel): def test_primary_key_from_grain(gooddata_tpcds_model: GdDeclarativeModel): """Verify primary_key is derived from grain attributes' source columns.""" - result = gooddata_to_osi(gooddata_tpcds_model) + result = gooddata_to_ossie(gooddata_tpcds_model) sm = result["semantic_model"][0] store_sales = next(ds for ds in sm["datasets"] if ds["name"] == "store_sales") @@ -161,7 +161,7 @@ def test_primary_key_from_grain(gooddata_tpcds_model: GdDeclarativeModel): def test_attributes_become_dimension_fields(gooddata_tpcds_model: GdDeclarativeModel): """Verify GoodData attributes become Ossie fields with dimension metadata.""" - result = gooddata_to_osi(gooddata_tpcds_model) + result = gooddata_to_ossie(gooddata_tpcds_model) sm = result["semantic_model"][0] customer = next(ds for ds in sm["datasets"] if ds["name"] == "customer") @@ -178,7 +178,7 @@ def test_attributes_become_dimension_fields(gooddata_tpcds_model: GdDeclarativeM def test_facts_become_plain_fields(gooddata_tpcds_model: GdDeclarativeModel): """Verify GoodData facts become Ossie fields without dimension metadata.""" - result = gooddata_to_osi(gooddata_tpcds_model) + result = gooddata_to_ossie(gooddata_tpcds_model) sm = result["semantic_model"][0] store_sales = next(ds for ds in sm["datasets"] if ds["name"] == "store_sales") @@ -193,7 +193,7 @@ def test_facts_become_plain_fields(gooddata_tpcds_model: GdDeclarativeModel): def test_omitted_fixture_source_types_remain_unspecified(gooddata_tpcds_model: GdDeclarativeModel): """Verify absent GoodData source types are not promoted from field roles.""" - result = gooddata_to_osi(gooddata_tpcds_model) + result = gooddata_to_ossie(gooddata_tpcds_model) store_sales = next( ds for ds in result["semantic_model"][0]["datasets"] if ds["name"] == "store_sales" ) @@ -206,7 +206,7 @@ def test_omitted_fixture_source_types_remain_unspecified(gooddata_tpcds_model: G def test_maql_expressions(gooddata_tpcds_model: GdDeclarativeModel): """Verify MAQL dialect expressions are generated for fields.""" - result = gooddata_to_osi(gooddata_tpcds_model) + result = gooddata_to_ossie(gooddata_tpcds_model) sm = result["semantic_model"][0] store_sales = next(ds for ds in sm["datasets"] if ds["name"] == "store_sales") @@ -224,7 +224,7 @@ def test_maql_expressions(gooddata_tpcds_model: GdDeclarativeModel): def test_references_become_relationships(gooddata_tpcds_model: GdDeclarativeModel): """Verify GoodData references become Ossie relationships.""" - result = gooddata_to_osi(gooddata_tpcds_model) + result = gooddata_to_ossie(gooddata_tpcds_model) sm = result["semantic_model"][0] rels = sm["relationships"] @@ -237,7 +237,7 @@ def test_references_become_relationships(gooddata_tpcds_model: GdDeclarativeMode def test_date_instance_converted(gooddata_tpcds_model: GdDeclarativeModel): """Verify date instances become Ossie datasets with custom_extensions.""" - result = gooddata_to_osi(gooddata_tpcds_model) + result = gooddata_to_ossie(gooddata_tpcds_model) sm = result["semantic_model"][0] date_ds = next(ds for ds in sm["datasets"] if ds["name"] == "date_dim") @@ -253,7 +253,7 @@ def test_date_instance_converted(gooddata_tpcds_model: GdDeclarativeModel): def test_labels_in_custom_extensions(gooddata_tpcds_model: GdDeclarativeModel): """Verify attribute labels are preserved in custom_extensions.""" - result = gooddata_to_osi(gooddata_tpcds_model) + result = gooddata_to_ossie(gooddata_tpcds_model) sm = result["semantic_model"][0] customer = next(ds for ds in sm["datasets"] if ds["name"] == "customer") @@ -272,7 +272,7 @@ def test_labels_in_custom_extensions(gooddata_tpcds_model: GdDeclarativeModel): def test_data_source_id_extension(gooddata_tpcds_model: GdDeclarativeModel): """Verify data_source_id is stored in model-level custom_extensions.""" - result = gooddata_to_osi(gooddata_tpcds_model, data_source_id="my_pg") + result = gooddata_to_ossie(gooddata_tpcds_model, data_source_id="my_pg") sm = result["semantic_model"][0] assert "custom_extensions" in sm diff --git a/converters/gooddata/tests/test_osi_to_gooddata.py b/converters/gooddata/tests/test_ossie_to_gooddata.py similarity index 90% rename from converters/gooddata/tests/test_osi_to_gooddata.py rename to converters/gooddata/tests/test_ossie_to_gooddata.py index 7848eec4..5f44be70 100644 --- a/converters/gooddata/tests/test_osi_to_gooddata.py +++ b/converters/gooddata/tests/test_ossie_to_gooddata.py @@ -23,10 +23,10 @@ import pytest -from ossie_gooddata.osi_to_gooddata import ( +from ossie_gooddata.ossie_to_gooddata import ( _convert_to_attribute, _convert_to_fact, - osi_to_gooddata, + ossie_to_gooddata, ) @@ -230,7 +230,7 @@ def test_temporal_datatype_does_not_turn_regular_dataset_into_date_instance(): ] } - result = osi_to_gooddata(model) + result = ossie_to_gooddata(model) assert result.ldm.date_instances == [] event_times = next(ds for ds in result.ldm.datasets if ds.id == "event_times") @@ -262,24 +262,24 @@ def test_legacy_all_explicit_time_fields_still_create_date_instance(): ] } - result = osi_to_gooddata(model) + result = ossie_to_gooddata(model) assert result.ldm.datasets == [] assert [date_instance.id for date_instance in result.ldm.date_instances] == ["calendar_dates"] -def test_basic_conversion(osi_tpcds_dict: dict): +def test_basic_conversion(ossie_tpcds_dict: dict): """Verify basic structure of Ossie → GoodData conversion.""" - result = osi_to_gooddata(osi_tpcds_dict, data_source_id="tpcds") + result = ossie_to_gooddata(ossie_tpcds_dict, data_source_id="tpcds") # 4 regular datasets (date_dim becomes a date instance) assert len(result.ldm.datasets) == 4 assert len(result.ldm.date_instances) == 1 -def test_dataset_ids(osi_tpcds_dict: dict): +def test_dataset_ids(ossie_tpcds_dict: dict): """Verify dataset IDs are preserved.""" - result = osi_to_gooddata(osi_tpcds_dict) + result = ossie_to_gooddata(ossie_tpcds_dict) ids = {ds.id for ds in result.ldm.datasets} assert "store_sales" in ids assert "customer" in ids @@ -287,9 +287,9 @@ def test_dataset_ids(osi_tpcds_dict: dict): assert "store" in ids -def test_date_dimension_detected(osi_tpcds_dict: dict): +def test_date_dimension_detected(ossie_tpcds_dict: dict): """Verify Ossie dataset with date_dimension extension becomes a GoodData date instance.""" - result = osi_to_gooddata(osi_tpcds_dict) + result = ossie_to_gooddata(ossie_tpcds_dict) assert len(result.ldm.date_instances) == 1 di = result.ldm.date_instances[0] @@ -298,9 +298,9 @@ def test_date_dimension_detected(osi_tpcds_dict: dict): assert "YEAR" in di.granularities -def test_dimension_fields_become_attributes(osi_tpcds_dict: dict): +def test_dimension_fields_become_attributes(ossie_tpcds_dict: dict): """Verify Ossie fields with dimension metadata become GoodData attributes.""" - result = osi_to_gooddata(osi_tpcds_dict) + result = ossie_to_gooddata(ossie_tpcds_dict) customer = next(ds for ds in result.ldm.datasets if ds.id == "customer") # c_customer_sk, c_first_name, c_last_name are all dimensions @@ -308,9 +308,9 @@ def test_dimension_fields_become_attributes(osi_tpcds_dict: dict): assert len(customer.facts) == 0 -def test_non_dimension_fields_become_facts(osi_tpcds_dict: dict): +def test_non_dimension_fields_become_facts(ossie_tpcds_dict: dict): """Verify Ossie fields without dimension become GoodData facts.""" - result = osi_to_gooddata(osi_tpcds_dict) + result = ossie_to_gooddata(ossie_tpcds_dict) store_sales = next(ds for ds in result.ldm.datasets if ds.id == "store_sales") # 4 dimension fields -> attributes, 4 non-dimension -> facts @@ -318,9 +318,9 @@ def test_non_dimension_fields_become_facts(osi_tpcds_dict: dict): assert len(store_sales.facts) == 4 -def test_maql_expression_detection(osi_tpcds_dict: dict): +def test_maql_expression_detection(ossie_tpcds_dict: dict): """Verify MAQL expressions are used to detect fact vs attribute.""" - result = osi_to_gooddata(osi_tpcds_dict) + result = ossie_to_gooddata(ossie_tpcds_dict) store_sales = next(ds for ds in result.ldm.datasets if ds.id == "store_sales") fact_ids = {f.id for f in store_sales.facts} @@ -328,9 +328,9 @@ def test_maql_expression_detection(osi_tpcds_dict: dict): assert any("ss_net_profit" in fid for fid in fact_ids) -def test_grain_from_primary_key(osi_tpcds_dict: dict): +def test_grain_from_primary_key(ossie_tpcds_dict: dict): """Verify primary_key columns become grain attributes.""" - result = osi_to_gooddata(osi_tpcds_dict) + result = ossie_to_gooddata(ossie_tpcds_dict) store_sales = next(ds for ds in result.ldm.datasets if ds.id == "store_sales") grain_ids = {g.id for g in store_sales.grain} @@ -338,9 +338,9 @@ def test_grain_from_primary_key(osi_tpcds_dict: dict): assert len(grain_ids) == 2 -def test_relationships_become_references(osi_tpcds_dict: dict): +def test_relationships_become_references(ossie_tpcds_dict: dict): """Verify Ossie relationships become GoodData references.""" - result = osi_to_gooddata(osi_tpcds_dict) + result = ossie_to_gooddata(ossie_tpcds_dict) store_sales = next(ds for ds in result.ldm.datasets if ds.id == "store_sales") assert len(store_sales.references) == 4 @@ -352,9 +352,9 @@ def test_relationships_become_references(osi_tpcds_dict: dict): assert "store" in ref_targets -def test_source_column_from_ansi_sql(osi_tpcds_dict: dict): +def test_source_column_from_ansi_sql(ossie_tpcds_dict: dict): """Verify source columns are extracted from ANSI_SQL expressions.""" - result = osi_to_gooddata(osi_tpcds_dict) + result = ossie_to_gooddata(ossie_tpcds_dict) customer = next(ds for ds in result.ldm.datasets if ds.id == "customer") source_cols = {a.source_column for a in customer.attributes} @@ -362,9 +362,9 @@ def test_source_column_from_ansi_sql(osi_tpcds_dict: dict): assert "c_first_name" in source_cols -def test_data_source_table_id(osi_tpcds_dict: dict): +def test_data_source_table_id(ossie_tpcds_dict: dict): """Verify source string is parsed into dataSourceTableId.""" - result = osi_to_gooddata(osi_tpcds_dict, data_source_id="tpcds") + result = ossie_to_gooddata(ossie_tpcds_dict, data_source_id="tpcds") store_sales = next(ds for ds in result.ldm.datasets if ds.id == "store_sales") assert store_sales.data_source_table_id is not None diff --git a/converters/gooddata/tests/test_roundtrip.py b/converters/gooddata/tests/test_roundtrip.py index 95b8a39b..3b36565d 100644 --- a/converters/gooddata/tests/test_roundtrip.py +++ b/converters/gooddata/tests/test_roundtrip.py @@ -23,7 +23,7 @@ import pytest -from ossie_gooddata.gooddata_to_osi import gooddata_to_osi +from ossie_gooddata.gooddata_to_ossie import gooddata_to_ossie from ossie_gooddata.models import ( GdAttribute, GdDataset, @@ -31,7 +31,7 @@ GdLdm, gd_model_to_dict, ) -from ossie_gooddata.osi_to_gooddata import osi_to_gooddata +from ossie_gooddata.ossie_to_gooddata import ossie_to_gooddata def _model_with_attribute(source_type: str | None) -> GdDeclarativeModel: @@ -61,21 +61,21 @@ def _model_with_attribute(source_type: str | None) -> GdDeclarativeModel: ) def test_roundtrip_preserves_native_source_types(source_type: str): """Verify every directly mapped GoodData source type round-trips.""" - ossie = gooddata_to_osi(_model_with_attribute(source_type)) - result = osi_to_gooddata(ossie) + ossie = gooddata_to_ossie(_model_with_attribute(source_type)) + result = ossie_to_gooddata(ossie) assert result.ldm.datasets[0].attributes[0].source_column_data_type == source_type def test_roundtrip_preserves_unknown_source_type_through_opaque(): """Verify an unknown GoodData type round-trips through Opaque extension data.""" - ossie = gooddata_to_osi(_model_with_attribute("CUSTOM_TYPE")) + ossie = gooddata_to_ossie(_model_with_attribute("CUSTOM_TYPE")) field = ossie["semantic_model"][0]["datasets"][0]["fields"][0] assert field["datatype"] == "Opaque" assert json.loads(field["custom_extensions"][0]["data"])["source_column_data_type"] == "CUSTOM_TYPE" - result = osi_to_gooddata(ossie) + result = ossie_to_gooddata(ossie) assert result.ldm.datasets[0].attributes[0].source_column_data_type == "CUSTOM_TYPE" @@ -83,9 +83,9 @@ def test_roundtrip_keeps_missing_source_type_unasserted(): """Verify a missing input type stays absent in Ossie and serialized GoodData.""" model = _model_with_attribute(None) - ossie = gooddata_to_osi(model) + ossie = gooddata_to_ossie(model) field = ossie["semantic_model"][0]["datasets"][0]["fields"][0] - result = gd_model_to_dict(osi_to_gooddata(ossie)) + result = gd_model_to_dict(ossie_to_gooddata(ossie)) attribute = result["ldm"]["datasets"][0]["attributes"][0] assert "datatype" not in field @@ -95,10 +95,10 @@ def test_roundtrip_keeps_missing_source_type_unasserted(): def test_roundtrip_preserves_datasets(gooddata_tpcds_model: GdDeclarativeModel): """Verify GoodData → Ossie → GoodData preserves dataset count and IDs.""" # GoodData -> Ossie - ossie = gooddata_to_osi(gooddata_tpcds_model, model_name="roundtrip_test") + ossie = gooddata_to_ossie(gooddata_tpcds_model, model_name="roundtrip_test") # Ossie -> GoodData - result = osi_to_gooddata(ossie) + result = ossie_to_gooddata(ossie) original_ds_ids = {ds.id for ds in gooddata_tpcds_model.ldm.datasets} result_ds_ids = {ds.id for ds in result.ldm.datasets} @@ -110,8 +110,8 @@ def test_roundtrip_preserves_datasets(gooddata_tpcds_model: GdDeclarativeModel): def test_roundtrip_preserves_date_instances(gooddata_tpcds_model: GdDeclarativeModel): """Verify date instances survive the round trip.""" - ossie = gooddata_to_osi(gooddata_tpcds_model) - result = osi_to_gooddata(ossie) + ossie = gooddata_to_ossie(gooddata_tpcds_model) + result = ossie_to_gooddata(ossie) assert len(result.ldm.date_instances) == len(gooddata_tpcds_model.ldm.date_instances) @@ -123,8 +123,8 @@ def test_roundtrip_preserves_date_instances(gooddata_tpcds_model: GdDeclarativeM def test_roundtrip_preserves_references(gooddata_tpcds_model: GdDeclarativeModel): """Verify references/relationships survive the round trip.""" - ossie = gooddata_to_osi(gooddata_tpcds_model) - result = osi_to_gooddata(ossie) + ossie = gooddata_to_ossie(gooddata_tpcds_model) + result = ossie_to_gooddata(ossie) original_ss = next(ds for ds in gooddata_tpcds_model.ldm.datasets if ds.id == "store_sales") result_ss = next(ds for ds in result.ldm.datasets if ds.id == "store_sales") @@ -136,8 +136,8 @@ def test_roundtrip_preserves_references(gooddata_tpcds_model: GdDeclarativeModel def test_roundtrip_preserves_attribute_count(gooddata_tpcds_model: GdDeclarativeModel): """Verify attribute counts survive the round trip.""" - ossie = gooddata_to_osi(gooddata_tpcds_model) - result = osi_to_gooddata(ossie) + ossie = gooddata_to_ossie(gooddata_tpcds_model) + result = ossie_to_gooddata(ossie) for orig_ds in gooddata_tpcds_model.ldm.datasets: result_ds = next((ds for ds in result.ldm.datasets if ds.id == orig_ds.id), None) @@ -149,8 +149,8 @@ def test_roundtrip_preserves_attribute_count(gooddata_tpcds_model: GdDeclarative def test_roundtrip_preserves_fact_count(gooddata_tpcds_model: GdDeclarativeModel): """Verify fact counts survive the round trip.""" - ossie = gooddata_to_osi(gooddata_tpcds_model) - result = osi_to_gooddata(ossie) + ossie = gooddata_to_ossie(gooddata_tpcds_model) + result = ossie_to_gooddata(ossie) for orig_ds in gooddata_tpcds_model.ldm.datasets: result_ds = next((ds for ds in result.ldm.datasets if ds.id == orig_ds.id), None) diff --git a/converters/honeydew/README.md b/converters/honeydew/README.md index df0b35f7..644961c2 100644 --- a/converters/honeydew/README.md +++ b/converters/honeydew/README.md @@ -17,20 +17,20 @@ under the License. --> -# OSI ↔ Honeydew Converter +# Ossie ↔ Honeydew Converter -Bidirectional converter between [OSI](../../core-spec/spec.md) semantic models and [Honeydew](https://honeydew.ai/docs) workspace YAML. +Bidirectional converter between [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 | +| `ossie-to-honeydew` | Single Ossie YAML file | Honeydew workspace directory | +| `honeydew-to-ossie` | 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 honeydew-ossie ossie-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 honeydew-ossie honeydew-to-ossie -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..1a2584a1 100644 --- a/converters/honeydew/pyproject.toml +++ b/converters/honeydew/pyproject.toml @@ -26,9 +26,9 @@ dev = [ ] [project] -name = "honeydew-osi" +name = "honeydew-ossie" version = "0.2.0.dev0" -description = "Bidirectional converter between Honeydew workspace YAML and OSI semantic model" +description = "Bidirectional converter between Honeydew workspace YAML and Ossie semantic model" 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" +honeydew-ossie = "honeydew_ossie.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/honeydew_ossie"] [tool.pytest.ini_options] testpaths = ["tests"] diff --git a/converters/honeydew/src/honeydew_osi/__init__.py b/converters/honeydew/src/honeydew_ossie/__init__.py similarity index 100% rename from converters/honeydew/src/honeydew_osi/__init__.py rename to converters/honeydew/src/honeydew_ossie/__init__.py diff --git a/converters/honeydew/src/honeydew_osi/converter.py b/converters/honeydew/src/honeydew_ossie/converter.py similarity index 81% rename from converters/honeydew/src/honeydew_osi/converter.py rename to converters/honeydew/src/honeydew_ossie/converter.py index e3f78f4f..ee6c092d 100644 --- a/converters/honeydew/src/honeydew_osi/converter.py +++ b/converters/honeydew/src/honeydew_ossie/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 + python converter.py ossie-to-honeydew -i input.yaml -o output_dir/ + python converter.py honeydew-to-ossie -i workspace_dir/ -o output.yaml """ import argparse @@ -38,23 +38,23 @@ 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" _HD_ATTR_KEYS = ("display_name", "hidden", "folder", "format_string", "timegrain") 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. +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. The caller writes these to disk under the desired output directory. @@ -73,10 +73,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: OSI YAML document as a string. + ossie_yaml_str: Ossie YAML document as a string. Returns: Dict of {relative_path: yaml_content}. @@ -84,14 +84,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 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: + if version_str != SUPPORTED_OSSIE_VERSION: raise HoneydewConversionError( - f"Unsupported OSI version '{version_str}'. Supported: {SUPPORTED_OSI_VERSION}" + f"Unsupported Ossie version '{version_str}'. Supported: {SUPPORTED_OSSIE_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" ) @@ -122,7 +122,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, @@ -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]] = [] @@ -180,7 +180,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") @@ -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) @@ -203,7 +203,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, @@ -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 @@ -286,13 +286,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 OSI fields that have no Honeydew native equivalent - entity_meta = _build_osi_metadata( + # Preserve Ossie fields that have no Honeydew native equivalent + entity_meta = _build_ossie_metadata( ai_context=ai_context, unique_keys=unique_keys, custom_extensions=ds_ext or None, @@ -306,7 +306,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 +349,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 +366,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 +437,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 +453,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") @@ -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. +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 - 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. @@ -554,7 +554,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 +564,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 +622,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 +634,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 +676,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} @@ -690,22 +690,22 @@ 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 - osi_meta = entity_data.get("osi_meta") or {} - if osi_meta.get("ai_context"): - ds["ai_context"] = osi_meta["ai_context"] + # Restore Ossie-only fields preserved in Honeydew metadata + 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 OSI source, de-duplicated. - unique_keys = [list(uk) for uk in (osi_meta.get("unique_keys") or [])] + # from an Ossie source, de-duplicated. + 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 +729,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 OSI) - 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 +763,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 +784,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 +792,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,8 +812,8 @@ 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 []) - # Always mark as calculated_attribute so OSI → Honeydew routes it correctly + 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, "data": json.dumps(dict({"type": "calculated_attribute", "entity": entity_name}, **calc_honeydew_extra)), @@ -828,16 +828,16 @@ 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} 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( +def _honeydew_relation_to_ossie( rel: dict[str, Any], entity_name: str, seen: set[tuple], @@ -871,21 +871,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 +895,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,25 +906,25 @@ 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 # ───────────────────────────────────────────────────────────────────────────── -# OSI metadata helpers — store/restore OSI fields in Honeydew metadata sections +# Ossie metadata helpers — store/restore Ossie fields in Honeydew metadata sections # ───────────────────────────────────────────────────────────────────────────── -def _build_osi_metadata( +def _build_ossie_metadata( *, ai_context: Any = None, label: str | None = None, @@ -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: @@ -949,13 +949,13 @@ 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 OSI-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 "") != _OSSIE_METADATA_SECTION: continue result: dict[str, Any] = {} for item in (section.get("metadata") or []): @@ -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,25 +1018,25 @@ 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("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 → OSI 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="OSI YAML output file") + 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 +1052,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_honeydew_osi_converter.py b/converters/honeydew/tests/test_honeydew_ossie_converter.py similarity index 91% rename from converters/honeydew/tests/test_honeydew_osi_converter.py rename to converters/honeydew/tests/test_honeydew_ossie_converter.py index c3aaa75c..4f8a0a56 100644 --- a/converters/honeydew/tests/test_honeydew_osi_converter.py +++ b/converters/honeydew/tests/test_honeydew_ossie_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,39 +26,39 @@ import pytest import yaml -from honeydew_osi.converter import ( +from honeydew_ossie.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): - """OSI → Honeydew → OSI; returns the semantic model dict.""" - files = convert_osi_to_honeydew(_osi(model_dict)) +def _ossie_roundtrip(model_dict, tmp_path): + """Ossie → Honeydew → Ossie; returns the semantic 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 → 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) + 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", [ @@ -244,46 +244,46 @@ def test_pick_ansi_expression_non_dict_warns(): # ───────────────────────────────────────────────────────────────────────────── -# OSI metadata helpers +# Ossie metadata helpers # ───────────────────────────────────────────────────────────────────────────── 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_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 # ───────────────────────────────────────────────────────────────────────────── @@ -339,7 +339,7 @@ def test_check_safe_path(rel_path, expected): # ───────────────────────────────────────────────────────────────────────────── -# OSI → Honeydew: file content +# Ossie → Honeydew: file content # ───────────────────────────────────────────────────────────────────────────── _REL_MODEL = { @@ -418,7 +418,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 +456,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 +474,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 +488,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 +520,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 +559,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,39 +576,39 @@ 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"} # ───────────────────────────────────────────────────────────────────────────── -# Honeydew → OSI: full document +# Honeydew → Ossie: full document # ───────────────────────────────────────────────────────────────────────────── 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): @@ -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", @@ -786,35 +786,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,15 +825,15 @@ 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 - # 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", @@ -842,7 +842,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"]] @@ -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"]]}]}, @@ -972,24 +972,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"]} @@ -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", [ @@ -1049,7 +1049,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 +1260,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 +1275,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 +1287,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 +1303,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 +1316,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 +1330,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 +1338,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 +1349,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 +1396,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 +1415,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,19 +1433,19 @@ 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": []} ]}], })) output_dir = tmp_path / "out" result = subprocess.run( - [sys.executable, "-m", "honeydew_osi.converter", - "osi-to-honeydew", "-i", str(input_file), "-o", str(output_dir)], + [sys.executable, "-m", "honeydew_ossie.converter", + "ossie-to-honeydew", "-i", str(input_file), "-o", str(output_dir)], capture_output=True, text=True, ) assert result.returncode == 0 @@ -1454,7 +1454,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", @@ -1462,13 +1462,13 @@ def test_main_honeydew_to_osi(tmp_path): }]) output_file = tmp_path / "output.yaml" result = subprocess.run( - [sys.executable, "-m", "honeydew_osi.converter", - "honeydew-to-osi", "-i", str(tmp_path), "-o", str(output_file)], + [sys.executable, "-m", "honeydew_ossie.converter", + "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 +1481,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", "honeydew_osi.converter", - "osi-to-honeydew", "-i", str(input_file), "-o", str(output_dir)], + [sys.executable, "-m", "honeydew_ossie.converter", + "ossie-to-honeydew", "-i", str(input_file), "-o", str(output_dir)], capture_output=True, text=True, ) assert result.returncode == 1 diff --git a/converters/honeydew/uv.lock b/converters/honeydew/uv.lock index 1220f531..a71e79eb 100644 --- a/converters/honeydew/uv.lock +++ b/converters/honeydew/uv.lock @@ -12,7 +12,7 @@ wheels = [ ] [[package]] -name = "honeydew-osi" +name = "honeydew-ossie" version = "0.2.0.dev0" source = { editable = "." } dependencies = [ diff --git a/converters/omni/README.md b/converters/omni/README.md index 695e1fdd..a4f2a57d 100644 --- a/converters/omni/README.md +++ b/converters/omni/README.md @@ -17,20 +17,20 @@ under the License. --> -# OSI <-> Omni converter +# Ossie <-> Omni converter -Bidirectional, offline conversion between an OSI semantic model and +Bidirectional, offline conversion between an Ossie semantic model and [Omni](https://docs.omni.co/modeling) semantic model files. No Omni connection required. An Omni model is a *directory* of YAML files rather than a single document, so -this converter maps one OSI YAML document to/from the Omni model layout: +this converter maps one Ossie YAML document to/from the Omni model layout: ``` model.yaml # model-wide config (restored verbatim from a prior import) relationships.yaml # top-level list of join definitions -views/.view.yaml # one per OSI dataset -topics/.topic.yaml # one generated topic per OSI model (or restored originals) +views/.view.yaml # one per Ossie dataset +topics/.topic.yaml # one generated topic per Ossie model (or restored originals) ``` Import also accepts the layout Omni's API/IDE emits (`omni models yaml-get`, @@ -40,19 +40,19 @@ schema-qualified view names (`delighted__response`) taken from each file's `# Reference this view as ...` header (falling back to the file's basename). Original file paths are preserved through a round trip. -- **Export** (`osi-omni export`): OSI -> Omni files. Datasets become views, +- **Export** (`ossie-omni export`): Ossie -> Omni files. Datasets become views, relationships become `relationships.yaml` joins, metrics become measures on the view they reference, and the model becomes a topic (rooted at the fact/FK-sink dataset, or `--base-view`). -- **Import** (`osi-omni import`): Omni files -> OSI. Omni features OSI has no +- **Import** (`ossie-omni import`): Omni files -> Ossie. Omni features Ossie has no native field for are preserved in `custom_extensions[OMNI]`, so - **Omni -> OSI -> Omni is lossless**. + **Omni -> Ossie -> Omni is lossless**. -On **export** (OSI -> Omni), OSI features with no Omni slot -- `unique_keys`, +On **export** (Ossie -> Omni), Ossie features with no Omni slot -- `unique_keys`, relationship `ai_context`, dataset-level `ai_context` synonyms, model-level `ai_context` synonyms/examples, foreign-vendor `custom_extensions`, fields and metrics without a usable dialect -- are **dropped with a warning**. On -**import** (Omni -> OSI), Omni-only features (formats, timeframes, hidden/tags, +**import** (Omni -> Ossie), Omni-only features (formats, timeframes, hidden/tags, topic curation, the model file, ...) are instead **preserved** in `custom_extensions[OMNI]`. Any input that breaks a [requirement](#requirements) **raises a `ConversionError`** -- the converter @@ -61,7 +61,7 @@ never silently drops a field or produces an invalid result. ## Installation ```bash -pip install osi-omni # once published to PyPI +pip install ossie-omni # once published to PyPI # or, from a checkout of this directory: pip install -e . ``` @@ -73,32 +73,32 @@ The only runtime dependency is `PyYAML`. Python 3.9+. ### Command line ```bash -osi-omni export -i model.yaml -o omni_model/ [--base-view orders] [--dialect SNOWFLAKE] -osi-omni import -i omni_model/ [-o model.yaml] [--name my_model] [--topic orders] +ossie-omni export -i model.yaml -o omni_model/ [--base-view orders] [--dialect SNOWFLAKE] +ossie-omni import -i omni_model/ [-o model.yaml] [--name my_model] [--topic orders] ``` `export` writes the Omni files into the `-o` directory. `import` reads a model directory (the [local-editor / git layout](https://docs.omni.co/guides/modeling/local-development), `.yaml`-suffixed or bare `.view`/`.topic` names both work); with no `-o` the -OSI YAML goes to stdout. `--base-view` picks the dataset the generated topic is +Ossie YAML goes to stdout. `--base-view` picks the dataset the generated topic is rooted at (default: the FK-sink dataset). `--topic` picks which topic's -description/AI context map onto the OSI model when there are several. +description/AI context map onto the Ossie model when there are several. ### Python API ```python -from osi_omni import convert_osi_to_omni, convert_omni_to_osi +from ossie_omni import convert_ossie_to_omni, convert_omni_to_ossie -files = convert_osi_to_omni(osi_yaml_str) # -> {relative filename: YAML str} -osi_yaml = convert_omni_to_osi(files) # {relative filename: YAML str} -> str +files = convert_ossie_to_omni(ossie_yaml_str) # -> {relative filename: YAML str} +ossie_yaml = convert_omni_to_ossie(files) # {relative filename: YAML str} -> str ``` ## Mapping Each row maps in both directions; the **Notes** flag where a behavior is -specific to **export** (OSI -> Omni) or **import** (Omni -> OSI). +specific to **export** (Ossie -> Omni) or **import** (Omni -> Ossie). -| OSI | Omni | Notes | +| Ossie | Omni | Notes | |---|---|---| | `semantic_model.name` | topic file name | Import: the mapped topic's name (override with `--name`). | | `model.description` / `ai_context.instructions` | topic `description` / `ai_context` | Import: taken from the sole topic, or `--topic`. | @@ -117,7 +117,7 @@ specific to **export** (OSI -> Omni) or **import** (Omni -> OSI). | `relationship.name` | -- | Regenerated as `_to_` on import (suffixed `_2`, `_3`, ... when several joins share a view pair). | | metric | measure on the view its expression references (else the base view) | `AGG(view.field)` <-> `aggregate_type` + `sql: ${field}`; `COUNT(*)` <-> `aggregate_type: count`; anything else <-> a raw-`sql` measure. | | `metric.description` / `ai_context` | measure `description` / `synonyms` / `ai_context` | | -| `custom_extensions[OMNI]` | everything Omni-only | Import stashes; export restores -- keeping `Omni -> OSI -> Omni` lossless. | +| `custom_extensions[OMNI]` | everything Omni-only | Import stashes; export restores -- keeping `Omni -> Ossie -> Omni` lossless. | **Stashed on import** (and restored on export): the model file (verbatim), topics (verbatim, minus the natively-mapped description/AI context), original @@ -125,15 +125,15 @@ file paths, view extras (`label`, `hidden`, `tags`, view-level `filters:`, ...), dimension extras (`format`, `group_label`, exact `timeframes`, original `sql`, ...), present-but-empty metadata (`description: ''`), join extras (declared `join_type`/`relationship_type`, `reversible`, `where_sql`, aliases, -non-canonical `on_sql`), joins OSI cannot represent (non-equi/range joins, +non-canonical `on_sql`), joins Ossie cannot represent (non-equi/range joins, joins touching a query or extends-only view -- restored at their original positions), fields/measures whose sql uses Omni template (`{{...}}`) syntax, non-reconstructible measures (filtered, `percentile`/`list`/`*_distinct_on`, -raw-SQL), and files with no OSI form (query views, extends-only views, +raw-SQL), and files with no Ossie form (query views, extends-only views, unrecognized files). **Expression dialects**: Omni SQL is the SQL of the model's database -connection, and the OSI dialect enum has no `OMNI` entry -- so import emits +connection, and the Ossie dialect enum has no `OMNI` entry -- so import emits `ANSI_SQL` expressions, and export prefers `ANSI_SQL` with `--dialect` prepending a warehouse dialect (e.g. `SNOWFLAKE` for a Snowflake-backed Omni model). A field/metric with neither is dropped with a warning. @@ -155,17 +155,17 @@ something invalid) when an input breaks one of these: - Exported `on_sql` references columns as `${view.column}`. Omni resolves these against the schema layer, which auto-generates a dimension per physical - column, so the reference is valid even when the OSI model declares no field + column, so the reference is valid even when the Ossie model declares no field for the column. - `dimension: {is_time: false}` is equivalent to omitting `dimension` and is normalized away on a round trip. -- One generated topic per OSI model; multi-path (aliased) join fan-out and +- One generated topic per Ossie model; multi-path (aliased) join fan-out and Omni query views, extends-only views, non-equi joins, composite topics, access grants/filters, and templated filters are stash-and-restore only (no - OSI semantics). + Ossie semantics). - A model containing *only* query/extends views (e.g. a pure GA4-export model) has nothing to convert and is rejected. -- OSI metric order is regrouped by view on import (order is not semantic). +- Ossie metric order is regrouped by view on import (order is not semantic). ## Development @@ -180,8 +180,8 @@ Example-based unit tests plus Hypothesis property-based round-trip tests ## Future effort -Both the OSI specification and Omni's model YAML are still evolving. As either +Both the Ossie specification and Omni's model YAML are still evolving. As either side adds or changes fields, this converter will be updated to track them -- extending the mapping and coverage in both directions (query views, composite -topics, measure filters as first-class OSI once the spec grows a slot for +topics, measure filters as first-class Ossie once the spec grows a slot for them) to keep the conversion current. diff --git a/converters/omni/pyproject.toml b/converters/omni/pyproject.toml index da22f010..4204a216 100644 --- a/converters/omni/pyproject.toml +++ b/converters/omni/pyproject.toml @@ -26,7 +26,7 @@ dev = [ ] [project] -name = "osi-omni" +name = "ossie-omni" version = "0.2.0.dev0" description = "Bidirectional converter between Apache Ossie semantic models and Omni semantic model files" authors = [{ name = "Apache Software Foundation", email = "dev@ossie.apache.org" }] @@ -43,14 +43,14 @@ dependencies = [ ] [project.scripts] -osi-omni = "osi_omni.cli:main" +ossie-omni = "ossie_omni.cli:main" [project.urls] homepage = "https://ossie.apache.org/" repository = "https://github.com/apache/ossie/" [tool.hatch.build.targets.wheel] -packages = ["src/osi_omni"] +packages = ["src/ossie_omni"] [tool.pytest.ini_options] testpaths = ["tests"] diff --git a/converters/omni/src/osi_omni/__init__.py b/converters/omni/src/ossie_omni/__init__.py similarity index 70% rename from converters/omni/src/osi_omni/__init__.py rename to converters/omni/src/ossie_omni/__init__.py index ca2ffdf7..4a6bd332 100644 --- a/converters/omni/src/osi_omni/__init__.py +++ b/converters/omni/src/ossie_omni/__init__.py @@ -15,19 +15,19 @@ # specific language governing permissions and limitations # under the License. -"""Bidirectional converter between OSI semantic models and Omni semantic model +"""Bidirectional converter between Ossie semantic models and Omni semantic model files (model.yaml / relationships.yaml / views/*.view.yaml / topics/*.topic.yaml). -Pure offline transforms: OSI YAML string <-> {relative filename: YAML string}. +Pure offline transforms: Ossie YAML string <-> {relative filename: YAML string}. - from osi_omni import convert_osi_to_omni, convert_omni_to_osi + from ossie_omni import convert_ossie_to_omni, convert_omni_to_ossie """ from ._common import ConversionError -from .omni_to_osi import convert_omni_to_osi -from .osi_to_omni import convert_osi_to_omni +from .omni_to_ossie import convert_omni_to_ossie +from .ossie_to_omni import convert_ossie_to_omni __all__ = [ "ConversionError", - "convert_omni_to_osi", - "convert_osi_to_omni", + "convert_omni_to_ossie", + "convert_ossie_to_omni", ] diff --git a/converters/omni/src/osi_omni/_common.py b/converters/omni/src/ossie_omni/_common.py similarity index 88% rename from converters/omni/src/osi_omni/_common.py rename to converters/omni/src/ossie_omni/_common.py index ad799bca..a8eb6935 100644 --- a/converters/omni/src/osi_omni/_common.py +++ b/converters/omni/src/ossie_omni/_common.py @@ -15,13 +15,13 @@ # specific language governing permissions and limitations # under the License. -"""Shared helpers for the OSI <-> Omni converters. +"""Shared helpers for the Ossie <-> Omni converters. Both directions are pure offline YAML transforms. The cross-cutting concerns live here: version constants, the dialect preference order, the `custom_extensions` stash protocol, Omni file-name conventions, identifier sanitization, and the `${...}` reference translation between Omni SQL and the plain column references -OSI expressions use. +Ossie expressions use. """ import datetime @@ -30,14 +30,14 @@ import yaml -# OSI semantic model spec version this converter targets (see core-spec). -OSI_VERSION = "0.2.0.dev0" +# Ossie semantic model spec version this converter targets (see core-spec). +OSSIE_VERSION = "0.2.0.dev0" # Vendor id used for the `custom_extensions` stash. VENDOR = "OMNI" # Omni SQL is the SQL of the model's database connection, so there is no OMNI -# entry in the OSI dialect enum. Import emits ANSI_SQL; export prefers ANSI_SQL +# entry in the Ossie dialect enum. Import emits ANSI_SQL; export prefers ANSI_SQL # and lets the caller prepend a warehouse dialect (e.g. SNOWFLAKE) that the # actual connection would accept. DIALECT_ANSI = "ANSI_SQL" @@ -57,7 +57,7 @@ REL_ONE_TO_MANY = "one_to_many" # The timeframes Omni applies to a time dimension by default; used to represent -# OSI `dimension.is_time` when no exact list is stashed. +# Ossie `dimension.is_time` when no exact list is stashed. DEFAULT_TIMEFRAMES = ["raw", "date", "week", "month", "quarter", "year"] # A valid Omni identifier (view, dimension, measure, topic name). Broader than @@ -109,7 +109,7 @@ def require_str(obj, key, what): # would corrupt Omni string values (a label "On", a week_start_day, a synonym). # The Loader below uses YAML 1.2 booleans (only true/false); the Dumper # force-quotes bool-like string tokens so the output round-trips through a 1.1 -# reader too. Same approach as the osi-databricks converter. +# reader too. Same approach as the ossie-databricks converter. class _Yaml12Loader(yaml.SafeLoader): """SafeLoader with YAML 1.2 boolean semantics.""" @@ -172,7 +172,7 @@ def is_omni_name(name): def sanitize_name(name, what, taken): - """Coerce an OSI name into a valid Omni identifier. + """Coerce an Ossie name into a valid Omni identifier. A name that is already a valid Omni identifier passes through untouched (leading/trailing underscores and camelCase are legal and occur in real @@ -191,7 +191,7 @@ def sanitize_name(name, what, taken): if out.lower() in taken: raise ConversionError( f"{what} '{name}' sanitizes to '{out}', which collides with another " - f"name; rename it in the OSI model." + f"name; rename it in the Ossie model." ) return out @@ -213,19 +213,19 @@ def topic_file(topic_name): def _json_default(o): for tag, cls in _JSON_TEMPORAL.items(): if type(o) is cls: - return {"__osi_omni__": tag, "v": o.isoformat()} + return {"__ossie_omni__": tag, "v": o.isoformat()} raise TypeError(f"Object of type {type(o).__name__} is not JSON serializable") def _json_object_hook(d): - cls = _JSON_TEMPORAL.get(d.get("__osi_omni__", "")) - if cls is not None and set(d) == {"__osi_omni__", "v"}: + cls = _JSON_TEMPORAL.get(d.get("__ossie_omni__", "")) + if cls is not None and set(d) == {"__ossie_omni__", "v"}: return cls.fromisoformat(d["v"]) return d def read_stash(obj): - """Return the OMNI stash dict on an OSI object, or {} if absent. + """Return the OMNI stash dict on an Ossie object, or {} if absent. The `_v` version marker is stripped from the returned dict. """ @@ -241,7 +241,7 @@ def read_stash(obj): def write_stash(obj, data): """Attach an OMNI `custom_extensions` entry holding `data` (a dict). - No-op when `data` is empty, so hand-authored OSI stays clean. Merges into an + No-op when `data` is empty, so hand-authored Ossie stays clean. Merges into an existing OMNI entry if one is already present. """ if not data: @@ -266,8 +266,8 @@ def foreign_vendor_extensions(obj): ] -def pick_expression(osi_expression, preferred=None): - """Choose the SQL string for an OSI expression. +def pick_expression(ossie_expression, preferred=None): + """Choose the SQL string for an Ossie expression. Preference order: the caller-chosen warehouse dialect (Omni passes SQL through to the connection's database, so e.g. SNOWFLAKE SQL is valid on a Snowflake- @@ -276,7 +276,7 @@ def pick_expression(osi_expression, preferred=None): """ dialects = { d.get("dialect"): d.get("expression") - for d in (osi_expression or {}).get("dialects") or [] + for d in (ossie_expression or {}).get("dialects") or [] } expr = None if preferred: @@ -290,14 +290,14 @@ def pick_expression(osi_expression, preferred=None): def synonyms_of(ai_context): - """Extract the synonyms list from an OSI ai_context (object form only).""" + """Extract the synonyms list from an Ossie ai_context (object form only).""" if isinstance(ai_context, dict): return list(ai_context.get("synonyms") or []) return [] def instructions_of(ai_context): - """The free-text part of an OSI ai_context: the string itself, or the + """The free-text part of an Ossie ai_context: the string itself, or the object form's `instructions`.""" if isinstance(ai_context, str) and ai_context.strip(): return ai_context @@ -315,13 +315,13 @@ def instructions_of(ai_context): def quote_source_part(part): - """Quote one part of an OSI dotted source when it needs it.""" + """Quote one part of an Ossie dotted source when it needs it.""" p = str(part) return p if re.fullmatch(r"[A-Za-z0-9_$]+", p) else f'"{p}"' def parse_source(source, dataset_name): - """Split an OSI dataset `source` into Omni view placement. + """Split an Ossie dataset `source` into Omni view placement. Returns ("sql", sql_text) for a SELECT/WITH subquery source, or ("table", catalog_or_None, schema, table) for a dotted table reference @@ -358,7 +358,7 @@ def parse_source(source, dataset_name): def join_source(view): - """Rebuild an OSI dataset `source` string from an Omni view dict.""" + """Rebuild an Ossie dataset `source` string from an Omni view dict.""" if view.get("sql") is not None: return str(view["sql"]).strip() schema = view.get("schema") @@ -369,17 +369,17 @@ def join_source(view): return ".".join(quote_source_part(p) for p in parts) -def omni_sql_to_osi(sql, own_view): +def omni_sql_to_ossie(sql, own_view): """Translate Omni `${...}` references in a SQL string to the plain references - OSI expressions use. Returns (translated, changed). + Ossie expressions use. Returns (translated, changed). - `${TABLE}.col` -> `col` (a raw column of the owning view) - `${field}` -> `field` (same-view field) - `${own_view.field}` -> `field` (qualified same-view field) - `${other.field}` -> `other.field` - - `${view.field[tf]}` -> `view.field` (timeframe access has no OSI form; + - `${view.field[tf]}` -> `view.field` (timeframe access has no Ossie form; the caller warns and stashes the original) - OSI has no field-vs-column distinction, so both flavors flatten to names. + Ossie has no field-vs-column distinction, so both flavors flatten to names. """ changed = False @@ -388,7 +388,7 @@ def repl(m): changed = True body = m.group(1) if body == "TABLE": - return "__OSI_TABLE__" # handled below with its trailing dot + return "__OSSIE_TABLE__" # handled below with its trailing dot body = re.sub(r"\[[^\]]*\]$", "", body).strip() # drop [timeframe] if "." in body: head, rest = body.split(".", 1) @@ -397,7 +397,7 @@ def repl(m): return body out = _OMNI_REF_RE.sub(repl, sql) - out = out.replace("__OSI_TABLE__.", "").replace("__OSI_TABLE__", "") + out = out.replace("__OSSIE_TABLE__.", "").replace("__OSSIE_TABLE__", "") return out, changed @@ -406,8 +406,8 @@ def has_timeframe_ref(sql): return bool(re.search(r"\$\{[^}]*\[[^\]]*\][^}]*\}", sql or "")) -def osi_expr_refs_to_omni(expr, view_names): - """Rewrite `view.column` references in an OSI expression to Omni `${view.column}` +def ossie_expr_refs_to_omni(expr, view_names): + """Rewrite `view.column` references in an Ossie expression to Omni `${view.column}` form, for the known `view_names` only -- so a genuine schema-qualified table or an unrelated dotted token is left alone. Bare columns stay bare (raw columns are legal in Omni SQL).""" diff --git a/converters/omni/src/osi_omni/cli.py b/converters/omni/src/ossie_omni/cli.py similarity index 75% rename from converters/omni/src/osi_omni/cli.py rename to converters/omni/src/ossie_omni/cli.py index 17af0224..0223fb91 100644 --- a/converters/omni/src/osi_omni/cli.py +++ b/converters/omni/src/ossie_omni/cli.py @@ -15,14 +15,14 @@ # specific language governing permissions and limitations # under the License. -"""Command-line interface for the OSI <-> Omni converter. +"""Command-line interface for the Ossie <-> Omni converter. - osi-omni export -i model.yaml -o omni_model/ [--base-view orders] [--dialect SNOWFLAKE] - osi-omni import -i omni_model/ [-o model.yaml] [--name my_model] [--topic orders] + ossie-omni export -i model.yaml -o omni_model/ [--base-view orders] [--dialect SNOWFLAKE] + ossie-omni import -i omni_model/ [-o model.yaml] [--name my_model] [--topic orders] -`export` converts an OSI semantic model into an Omni model directory +`export` converts an Ossie semantic model into an Omni model directory (model.yaml / relationships.yaml / views/*.view.yaml / topics/*.topic.yaml); -`import` does the reverse. Import with no `-o` writes the OSI YAML to stdout; +`import` does the reverse. Import with no `-o` writes the Ossie YAML to stdout; export always needs `-o` (a directory). Conversions that drop information emit warnings to stderr. """ @@ -32,33 +32,33 @@ import sys from ._common import ConversionError -from .omni_to_osi import convert_omni_to_osi -from .osi_to_omni import convert_osi_to_omni +from .omni_to_ossie import convert_omni_to_ossie +from .ossie_to_omni import convert_ossie_to_omni def _build_parser(): - parser = argparse.ArgumentParser(prog="osi-omni", description=__doc__, + parser = argparse.ArgumentParser(prog="ossie-omni", description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) sub = parser.add_subparsers(dest="command") sub.required = True - exp = sub.add_parser("export", help="OSI semantic model -> Omni model directory") - exp.add_argument("-i", "--input", required=True, help="OSI YAML file") + exp = sub.add_parser("export", help="Ossie semantic model -> Omni model directory") + exp.add_argument("-i", "--input", required=True, help="Ossie YAML file") exp.add_argument("-o", "--output", required=True, help="output directory for the Omni model files") exp.add_argument("-b", "--base-view", help="dataset the generated topic is rooted at " "(default: the FK-sink dataset)") exp.add_argument("-d", "--dialect", - help="preferred OSI expression dialect (e.g. SNOWFLAKE); " + help="preferred Ossie expression dialect (e.g. SNOWFLAKE); " "ANSI_SQL is always the fallback") - imp = sub.add_parser("import", help="Omni model directory -> OSI semantic model YAML") + imp = sub.add_parser("import", help="Omni model directory -> Ossie semantic model YAML") imp.add_argument("-i", "--input", required=True, help="Omni model directory") - imp.add_argument("-o", "--output", help="output OSI YAML file (default: stdout)") - imp.add_argument("--name", help="OSI model name (default: the mapped topic's name)") + imp.add_argument("-o", "--output", help="output Ossie YAML file (default: stdout)") + imp.add_argument("--name", help="Ossie model name (default: the mapped topic's name)") imp.add_argument("--topic", - help="topic whose description/AI context map onto the OSI model " + help="topic whose description/AI context map onto the Ossie model " "(default: the sole topic, if there is exactly one)") return parser @@ -91,8 +91,8 @@ def main(argv=None): try: if args.command == "export": with open(args.input) as fh: - osi_yaml = fh.read() - files = convert_osi_to_omni(osi_yaml, base_view=args.base_view, + ossie_yaml = fh.read() + files = convert_ossie_to_omni(ossie_yaml, base_view=args.base_view, dialect=args.dialect) for rel, text in files.items(): dest = os.path.join(args.output, *rel.split("/")) @@ -102,7 +102,7 @@ def main(argv=None): print(f"Wrote {len(files)} file(s) to {args.output}", file=sys.stderr) else: files = _read_model_dir(args.input) - out = convert_omni_to_osi(files, model_name=args.name, topic=args.topic) + out = convert_omni_to_ossie(files, model_name=args.name, topic=args.topic) if args.output: with open(args.output, "w") as fh: fh.write(out) diff --git a/converters/omni/src/osi_omni/omni_to_osi.py b/converters/omni/src/ossie_omni/omni_to_ossie.py similarity index 89% rename from converters/omni/src/osi_omni/omni_to_osi.py rename to converters/omni/src/ossie_omni/omni_to_ossie.py index 75a1d3b2..97b6ed62 100644 --- a/converters/omni/src/osi_omni/omni_to_osi.py +++ b/converters/omni/src/ossie_omni/omni_to_ossie.py @@ -15,18 +15,18 @@ # specific language governing permissions and limitations # under the License. -"""Convert Omni semantic model files to an OSI semantic model. +"""Convert Omni semantic model files to an Ossie semantic model. Pure offline conversion. Accepts the Omni model-directory layout as a mapping of {relative filename: YAML string} -- `model.yaml`, `relationships.yaml`, -`views/*.view.yaml`, `topics/*.topic.yaml`. Omni features OSI has no native field +`views/*.view.yaml`, `topics/*.topic.yaml`. Omni features Ossie has no native field for (formats, timeframes, hidden/tags, topic curation, the model file itself, query views, extends-only views, non-equi joins, ...) are preserved in `custom_extensions[OMNI]` so that converting back reproduces the original files. See README.md. Usage (CLI): - osi-omni import -i omni_model/ [-o model.yaml] [--name NAME] [--topic TOPIC] + ossie-omni import -i omni_model/ [-o model.yaml] [--name NAME] [--topic TOPIC] """ import re @@ -36,7 +36,7 @@ ConversionError, DIALECT_ANSI, MODEL_FILE, - OSI_VERSION, + OSSIE_VERSION, REL_MANY_TO_ONE, REL_ONE_TO_MANY, RELATIONSHIPS_FILE, @@ -46,7 +46,7 @@ is_simple_identifier, join_source, load_yaml, - omni_sql_to_osi, + omni_sql_to_ossie, require_str, view_file, write_stash, @@ -90,18 +90,18 @@ def _canonical_view_name(text, basename): _DIM_NATIVE_KEYS = {"sql", "label", "description", "synonyms", "ai_context", "primary_key"} -# Measure keys the OSI metric represents natively (given a reconstructible +# Measure keys the Ossie metric represents natively (given a reconstructible # aggregate); any other key forces the full-measure stash. _MEASURE_NATIVE_KEYS = {"sql", "aggregate_type", "description", "synonyms", "ai_context"} -# aggregate_type values whose OSI expression the exporter can rebuild exactly. +# aggregate_type values whose Ossie expression the exporter can rebuild exactly. _SIMPLE_AGGS = {"sum": "SUM", "count": "COUNT", "average": "AVG", "min": "MIN", "max": "MAX", "median": "MEDIAN", "count_distinct": None} # Best-effort ANSI renderings for Omni-only aggregate types. The original -# measure is stashed verbatim, so the Omni -> OSI -> Omni trip stays lossless; -# the expression is what other OSI consumers see. +# measure is stashed verbatim, so the Omni -> Ossie -> Omni trip stays lossless; +# the expression is what other Ossie consumers see. _EXOTIC_AGGS = {"percentile", "list", "sum_distinct_on", "average_distinct_on", "median_distinct_on", "percentile_distinct_on"} @@ -112,12 +112,12 @@ def _canonical_view_name(text, basename): ) -def convert_omni_to_osi(files, model_name=None, topic=None): - """Convert Omni model files ({relative filename: YAML str}) to OSI YAML. +def convert_omni_to_ossie(files, model_name=None, topic=None): + """Convert Omni model files ({relative filename: YAML str}) to Ossie YAML. - `model_name` overrides the OSI model name (default: the mapped topic's name, + `model_name` overrides the Ossie model name (default: the mapped topic's name, else 'omni_model'). `topic` names the topic whose description/AI context map - onto the OSI model when the directory holds more than one topic. + onto the Ossie model when the directory holds more than one topic. """ if not isinstance(files, dict) or not files: raise ConversionError("expected a non-empty mapping of {filename: YAML}") @@ -125,16 +125,16 @@ def convert_omni_to_osi(files, model_name=None, topic=None): views, topics = {}, {} view_meta = {} # canonical name -> (file path, file basename) topic_paths = {} - unmapped_views = set() # view names present as files but with no OSI dataset + unmapped_views = set() # view names present as files but with no Ossie dataset model_yaml = None rel_entries = [] extra_files = {} for fname, text in files.items(): qv = _QUERY_VIEW_FILE_RE.search(fname) if qv: - # Query views are backed by a saved query, not a table; OSI has no + # Query views are backed by a saved query, not a table; Ossie has no # dataset form for them. Preserved verbatim, restored on export. - _warn(f"file '{fname}'", "query views have no OSI dataset form; " + _warn(f"file '{fname}'", "query views have no Ossie dataset form; " "preserved in custom_extensions only") unmapped_views.add(_canonical_view_name(text, qv.group(1))) extra_files[fname] = text @@ -147,7 +147,7 @@ def convert_omni_to_osi(files, model_name=None, topic=None): dict(parsed, table_name=parsed.get("table_name", mv.group(1)))) if source is None: # A view with no schema/sql of its own (an `extends`-only view) - # has no standalone OSI dataset form. Preserved verbatim. + # has no standalone Ossie dataset form. Preserved verbatim. _warn(f"view '{vname}'", "no `schema`/`sql` source (an extends-only view?); " "preserved in custom_extensions only") @@ -185,7 +185,7 @@ def convert_omni_to_osi(files, model_name=None, topic=None): "no convertible view files (*.view.yaml with a schema/sql source) " "found; nothing to convert") - # The mapped topic supplies the OSI model's name/description/ai_context. + # The mapped topic supplies the Ossie model's name/description/ai_context. mapped_name = None if topic is not None: if topic not in topics: @@ -215,7 +215,7 @@ def convert_omni_to_osi(files, model_name=None, topic=None): datasets.append(_convert_view(vname, view, view_meta[vname])) model["datasets"] = datasets - # A join OSI cannot represent -- one touching a view with no OSI dataset (a + # A join Ossie cannot represent -- one touching a view with no Ossie dataset (a # query view, an extends-only view), or a non-equi/cross join -- is stashed # verbatim with its position, so export rebuilds relationships.yaml in the # original order. Malformed entries still raise. @@ -226,14 +226,14 @@ def convert_omni_to_osi(files, model_name=None, topic=None): rel = None if endpoints & unmapped_views: _warn(f"relationship #{i + 1}", - "references a view with no OSI dataset form; preserved in " + "references a view with no Ossie dataset form; preserved in " "custom_extensions only") else: rel = _convert_relationship(entry, i, views) if rel is None: extra_rels.append({"index": i, "entry": entry}) continue - # OSI relationship names are unique per model; several (aliased) joins + # Ossie relationship names are unique per model; several (aliased) joins # between one view pair generate the same `_to_` -- suffix # the repeats. Export never reads the name, so this stays lossless. base, n, k = rel["name"], rel["name"], 2 @@ -282,7 +282,7 @@ def convert_omni_to_osi(files, model_name=None, topic=None): stash["extra_files"] = extra_files write_stash(model, stash) - return dump_yaml({"version": OSI_VERSION, "semantic_model": [model]}) + return dump_yaml({"version": OSSIE_VERSION, "semantic_model": [model]}) def _convert_view(vname, view, meta): @@ -313,12 +313,12 @@ def _convert_view(vname, view, meta): raw_compound = view.get("custom_compound_primary_key_sql") or [] compound = [] for c in raw_compound: - translated, _ = omni_sql_to_osi(str(c), vname) + translated, _ = omni_sql_to_ossie(str(c), vname) translated = translated.strip() compound.append(translated if is_simple_identifier(translated) else str(c)) # Omni mustache templating ({{# field.filter }} ...) in a field's sql has - # no SQL (or OSI) form at all; such dimensions/measures are stashed whole - # and dropped from the OSI model. Popped here so the later measure pass + # no SQL (or Ossie) form at all; such dimensions/measures are stashed whole + # and dropped from the Ossie model. Popped here so the later measure pass # sees only convertible measures. for kind, key in (("dimensions", "extra_dimensions"), ("measures", "extra_measures")): @@ -329,7 +329,7 @@ def _convert_view(vname, view, meta): for n in templated: _warn(f"{kind[:-1]} '{vname}.{n}'", "sql uses Omni template syntax ('{{'), which has no " - "OSI form; preserved in custom_extensions only") + "Ossie form; preserved in custom_extensions only") entries.pop(n) stash[key] = templated @@ -348,7 +348,7 @@ def _convert_view(vname, view, meta): ds["fields"] = fields # Stash the original compound-key list whenever the exporter could not - # rebuild it from the OSI primary_key alone (a `${view.field}` reference + # rebuild it from the Ossie primary_key alone (a `${view.field}` reference # that was normalized away, or an entry no dimension covers). if raw_compound and ( compound != [str(c) for c in raw_compound] @@ -374,7 +374,7 @@ def _convert_view(vname, view, meta): stash["file"] = fname extras = {k: v for k, v in view.items() if k not in _VIEW_NATIVE_KEYS} for key in ("description", "ai_context"): - # Present-but-empty metadata has no OSI slot; preserve it as an extra. + # Present-but-empty metadata has no Ossie slot; preserve it as an extra. if key in view and not view[key]: extras[key] = view[key] if extras: @@ -384,7 +384,7 @@ def _convert_view(vname, view, meta): def _convert_dimension(vname, dname, dim): - """Build one OSI field from an Omni dimension. Returns (field, column) where + """Build one Ossie field from an Omni dimension. Returns (field, column) where `column` is the underlying column used for key resolution (the translated expression when it is a bare column, else the dimension name).""" scope = f"dimension '{vname}.{dname}'" @@ -395,12 +395,12 @@ def _convert_dimension(vname, dname, dim): expr = dname # schema-layer default: the same-named physical column else: sql = str(sql) - expr, changed = omni_sql_to_osi(sql, vname) + expr, changed = omni_sql_to_ossie(sql, vname) if has_timeframe_ref(sql): - _warn(scope, "timeframe reference (${view.field[timeframe]}) has no OSI " + _warn(scope, "timeframe reference (${view.field[timeframe]}) has no Ossie " "form; flattened to the base field, original sql stashed") if changed or sql.strip() == dname: - # Stashed when the OSI expression differs from the Omni sql, and + # Stashed when the Ossie expression differs from the Omni sql, and # also when the sql is an explicit same-named bare column -- which # the exporter would otherwise normalize to the implicit # schema-layer default (no `sql:` key). @@ -429,8 +429,8 @@ def _convert_dimension(vname, dname, dim): if key == "timeframes": continue if key in _DIM_NATIVE_KEYS: - # A present-but-empty native value (description: '') has no OSI - # slot -- OSI omits empty metadata -- so it rides in the stash. + # A present-but-empty native value (description: '') has no Ossie + # slot -- Ossie omits empty metadata -- so it rides in the stash. if not value and not isinstance(value, bool) and key != "sql": stash[key] = value continue @@ -462,11 +462,11 @@ def _convert_relationship(entry, index, views): for clause in re.split(r"\s+AND\s+", on_sql, flags=re.IGNORECASE): m = _ON_CLAUSE_RE.match(clause) if not m: - # A valid Omni join OSI cannot express (a range/non-equi or cross + # A valid Omni join Ossie cannot express (a range/non-equi or cross # join). The caller stashes the entry verbatim. _warn(what, f"('{from_view}' -> '{to_view}'): on_sql clause " f"'{clause.strip()}' is not an equi-join of two " - f"${{view.field}} references, so it has no OSI " + f"${{view.field}} references, so it has no Ossie " f"relationship form; preserved in custom_extensions only") return None la, lf, ra, rf = m.groups() @@ -485,7 +485,7 @@ def _convert_relationship(entry, index, views): # The declared type/join_type are stashed verbatim whenever present -- even # when they restate an Omni default -- so export reproduces the exact file. stash = {} - # Export rebuilds on_sql from the OSI columns in canonical form; when that + # Export rebuilds on_sql from the Ossie columns in canonical form; when that # would not reproduce the original (an alias reference, reversed clause # sides, `and` casing, spacing, a field-to-column translation), the # original rides in the stash instead. @@ -498,14 +498,14 @@ def _convert_relationship(entry, index, views): if "relationship_type" in entry: stash["relationship_type"] = rel_type if rel_type == REL_ONE_TO_MANY: - # OSI `from` is always the many side; flip the orientation (the stashed + # Ossie `from` is always the many side; flip the orientation (the stashed # type tells export to flip back to the original one_to_many join). from_view, to_view = to_view, from_view from_cols, to_cols = to_cols, from_cols elif rel_type == "many_to_many": # one_to_one / many_to_many / assumed_many_to_one keep the declared # orientation. - _warn(what, "many_to_many has no OSI orientation (OSI `to` is the one " + _warn(what, "many_to_many has no Ossie orientation (Ossie `to` is the one " "side); orientation kept as declared, type preserved in " "custom_extensions") @@ -516,7 +516,7 @@ def _convert_relationship(entry, index, views): stash["join_type"] = entry["join_type"] if entry.get("where_sql"): stash["where_sql"] = entry["where_sql"] - _warn(what, "where_sql (join filter) has no OSI form; preserved in " + _warn(what, "where_sql (join filter) has no Ossie form; preserved in " "custom_extensions only") for key in ("reversible", "id", "join_from_view_as", "join_from_view_as_label", "join_to_view_as", "join_to_view_as_label"): @@ -537,11 +537,11 @@ def _field_column(view, fname): def _convert_measures(views, relationships, base_view): - """Turn every view's measures into OSI model-level metrics. + """Turn every view's measures into Ossie model-level metrics. A metric name is the measure name when globally unique, else `__` (the original name is stashed either way when it - matters). The full original measure is stashed whenever the OSI expression + matters). The full original measure is stashed whenever the Ossie expression alone cannot reconstruct it exactly. """ # The exporter re-derives measure placement from the expression (a @@ -577,7 +577,7 @@ def _convert_measure(vname, mname, metric_name, measure, effective_base): agg = measure.get("aggregate_type") sql = measure.get("sql") expr = None - # Reconstructible = the exporter can rebuild this measure from the OSI + # Reconstructible = the exporter can rebuild this measure from the Ossie # expression alone: only natively-mapped keys, and a sql that is either free # of `${...}` references or a lone same-view field reference (which the # exporter re-derives from the view's dimension names). Anything else keeps @@ -589,12 +589,12 @@ def _convert_measure(vname, mname, metric_name, measure, effective_base): ) if agg is None and sql is not None: - # A raw-SQL measure (the aggregate is written out in the sql). OSI + # A raw-SQL measure (the aggregate is written out in the sql). Ossie # metrics are model-level, so view qualifiers are kept, not stripped # (own_view=None below leaves `${view.field}` as `view.field`). # Always stashed: the exporter would otherwise re-parse a lone # aggregate call into a structured measure, changing the file. - expr, _ = omni_sql_to_osi(str(sql), own_view=None) + expr, _ = omni_sql_to_ossie(str(sql), own_view=None) reconstructible = False elif agg in _SIMPLE_AGGS: if agg == "count" and sql is None: @@ -608,7 +608,7 @@ def _convert_measure(vname, mname, metric_name, measure, effective_base): else: expr = f"{_SIMPLE_AGGS[agg]}({inner})" elif agg in _EXOTIC_AGGS: - # Best-effort ANSI so OSI consumers still see a metric; the verbatim + # Best-effort ANSI so Ossie consumers still see a metric; the verbatim # measure is stashed, so re-export is exact. inner = _measure_operand(vname, str(sql or "")) expr = _exotic_expr(agg, inner, measure) @@ -621,7 +621,7 @@ def _convert_measure(vname, mname, metric_name, measure, effective_base): f"{scope}: unknown aggregate_type '{agg}'") if measure.get("filters"): - _warn(scope, "measure filters have no OSI form; expression emitted " + _warn(scope, "measure filters have no Ossie form; expression emitted " "unfiltered, original measure preserved in custom_extensions") reconstructible = False @@ -661,10 +661,10 @@ def _convert_measure(vname, mname, metric_name, measure, effective_base): def _measure_operand(vname, sql): - """Translate a measure's operand sql to an OSI reference: a same-view field - or bare column becomes `view.name` (the qualified form OSI metrics use); + """Translate a measure's operand sql to an Ossie reference: a same-view field + or bare column becomes `view.name` (the qualified form Ossie metrics use); anything else keeps its view qualifiers and is left as-is.""" - inner, _ = omni_sql_to_osi(sql, own_view=None) + inner, _ = omni_sql_to_ossie(sql, own_view=None) inner = inner.strip() if is_simple_identifier(inner): return f"{vname}.{inner}" diff --git a/converters/omni/src/osi_omni/osi_to_omni.py b/converters/omni/src/ossie_omni/ossie_to_omni.py similarity index 92% rename from converters/omni/src/osi_omni/osi_to_omni.py rename to converters/omni/src/ossie_omni/ossie_to_omni.py index e2472b3f..43ca7e58 100644 --- a/converters/omni/src/osi_omni/osi_to_omni.py +++ b/converters/omni/src/ossie_omni/ossie_to_omni.py @@ -15,7 +15,7 @@ # specific language governing permissions and limitations # under the License. -"""Convert an OSI semantic model to Omni semantic model files. +"""Convert an Ossie semantic model to Omni semantic model files. Pure offline conversion -- no Omni connection required. Produces the Omni model-directory layout: one `views/.view.yaml` per dataset, a @@ -24,7 +24,7 @@ the original `model.yaml`. See README.md for the capability summary. Usage (CLI): - osi-omni export -i model.yaml -o omni_model/ [--base-view orders] [--dialect SNOWFLAKE] + ossie-omni export -i model.yaml -o omni_model/ [--base-view orders] [--dialect SNOWFLAKE] """ import re @@ -34,7 +34,7 @@ ConversionError, DEFAULT_TIMEFRAMES, MODEL_FILE, - OSI_VERSION, + OSSIE_VERSION, REL_MANY_TO_ONE, REL_ONE_TO_MANY, RELATIONSHIPS_FILE, @@ -43,7 +43,7 @@ instructions_of, is_simple_identifier, load_yaml, - osi_expr_refs_to_omni, + ossie_expr_refs_to_omni, parse_source, pick_expression, read_stash, @@ -64,7 +64,7 @@ def _warn(scope, msg): # `sql` is handled separately (it replaces the derived expression). _DIM_STASH_PASSTHROUGH_EXCLUDE = {"sql"} -# One OSI metric aggregate call maps to a structured Omni measure. +# One Ossie metric aggregate call maps to a structured Omni measure. _AGG_TO_OMNI = { "SUM": "sum", "COUNT": "count", @@ -79,28 +79,28 @@ def _warn(scope, msg): re.IGNORECASE | re.DOTALL, ) -# `view.column` -- a dotted reference an OSI metric uses to point into a dataset. +# `view.column` -- a dotted reference an Ossie metric uses to point into a dataset. _DOTTED_REF_RE = re.compile( r"(? OSI -> Omni restores the exact expression. + # Omni -> Ossie -> Omni restores the exact expression. if "sql" in stash: dim["sql"] = stash["sql"] elif is_simple_identifier(expr): @@ -358,7 +358,7 @@ def _convert_field(field, dname, expr, fscope): # else: the dimension inherits the same-named schema column -- no sql key. else: # Raw SQL over the view's own columns is legal in Omni sql. References to - # other views would need ${view.field} form; OSI field expressions are + # other views would need ${view.field} form; Ossie field expressions are # dataset-scoped, so this is emitted as-is. dim["sql"] = expr @@ -383,7 +383,7 @@ def _convert_field(field, dname, expr, fscope): is_time = field.get("datatype") in ("Date", "Time", "DateTime", "DateTimez") if is_time: # The stashed list wins even when empty (`timeframes: []` is a real - # Omni value); the default list is only for hand-authored OSI. + # Omni value); the default list is only for hand-authored Ossie. dim["timeframes"] = (stash["timeframes"] if "timeframes" in stash else list(DEFAULT_TIMEFRAMES)) elif "timeframes" in stash: @@ -416,7 +416,7 @@ def _convert_relationship(rel, view_names, imported=False): stash = read_stash(rel) from_view, to_view = view_names[rel["from"]], view_names[rel["to"]] if stash.get("relationship_type") == REL_ONE_TO_MANY: - # The import flipped a one_to_many join so the OSI `from` is the many + # The import flipped a one_to_many join so the Ossie `from` is the many # side; flip back to the originally declared orientation. from_view, to_view = to_view, from_view from_cols, to_cols = to_cols, from_cols @@ -435,9 +435,9 @@ def _convert_relationship(rel, view_names, imported=False): "${" + f"{from_view}.{fc}" + "} = ${" + f"{to_view}.{tc}" + "}" for fc, tc in zip(from_cols, to_cols) ) - # OSI orientation is many(from) -> one(to). The stash holds the declared + # Ossie orientation is many(from) -> one(to). The stash holds the declared # type/join_type verbatim (even an explicit Omni default). An imported join - # with no stashed key genuinely omitted it; fresh OSI states many_to_one. + # with no stashed key genuinely omitted it; fresh Ossie states many_to_one. if "relationship_type" in stash: entry["relationship_type"] = stash["relationship_type"] elif not imported: @@ -520,13 +520,13 @@ def _build_topic(model, base_vname, view_names, relationships): def _convert_metric(metric, resolve_base, view_names, dims_by_view, dialect): - """Map one OSI metric to an Omni measure. Returns (view_name, measure_name, + """Map one Ossie metric to an Omni measure. Returns (view_name, measure_name, measure_dict), or None when the metric has no usable expression.""" mname_raw = require_str(metric, "name", "metric") scope = f"metric '{mname_raw}'" stash = read_stash(metric) # The import qualifies a colliding measure name as `__` and - # stashes the original; restore it, else sanitize the OSI name. + # stashes the original; restore it, else sanitize the Ossie name. mname = stash.get("name") or sanitize_name(mname_raw, scope, set()) if "measure" in stash: @@ -578,7 +578,7 @@ def _convert_metric(metric, resolve_base, view_names, dims_by_view, dialect): measure = {"sql": inner, "aggregate_type": agg} target = None elif len(referenced) == 1: - measure = {"sql": osi_expr_refs_to_omni(inner, sanitized_views), + measure = {"sql": ossie_expr_refs_to_omni(inner, sanitized_views), "aggregate_type": agg} target = next(iter(referenced)) @@ -586,7 +586,7 @@ def _convert_metric(metric, resolve_base, view_names, dims_by_view, dialect): # Anything else (a ratio, a multi-view aggregate, window SQL) becomes a # raw-SQL measure; `view.col` references switch to `${view.col}` form. target = next(iter(referenced)) if len(referenced) == 1 else None - measure = {"sql": osi_expr_refs_to_omni(expr, sanitized_views)} + measure = {"sql": ossie_expr_refs_to_omni(expr, sanitized_views)} if len(referenced) > 1: _warn(scope, f"expression spans views {sorted(referenced)}; emitted as a " f"raw-SQL measure on the base view -- verify the join path") diff --git a/converters/omni/tests/_roundtrip_helpers.py b/converters/omni/tests/_roundtrip_helpers.py index 3c55d651..674fe56c 100644 --- a/converters/omni/tests/_roundtrip_helpers.py +++ b/converters/omni/tests/_roundtrip_helpers.py @@ -33,10 +33,10 @@ construction (documented inline), e.g.: - Omni names are generated already valid (lowercase snake_case), so the sanitizer never renames anything; - - OSI metric expressions reference *field names* (`view.field`), which survive + - Ossie metric expressions reference *field names* (`view.field`), which survive the `${field}` modeled-reference trip; a raw column that differs from its field's name would come back as the field name; - - OSI relationship names use the canonical `_to_` form the importer + - Ossie relationship names use the canonical `_to_` form the importer regenerates. Name fuzzing (collisions, reserved words) is left to the targeted unit tests, which assert the converter *rejects* or *warns on* those inputs. @@ -46,13 +46,13 @@ import string import warnings -from osi_omni import convert_omni_to_osi, convert_osi_to_omni -from osi_omni._common import OSI_VERSION, dump_yaml, load_yaml +from ossie_omni import convert_omni_to_ossie, convert_ossie_to_omni +from ossie_omni._common import OSSIE_VERSION, dump_yaml, load_yaml from _util import strip_normalized _AGGS = ["sum", "average", "min", "max", "median", "count_distinct"] -_OSI_AGGS = ["SUM", "AVG", "MIN", "MAX", "MEDIAN"] +_OSSIE_AGGS = ["SUM", "AVG", "MIN", "MAX", "MEDIAN"] # --- Rnd backend for offline (no hypothesis) runs -------------------------------- @@ -102,7 +102,7 @@ def next(self, prefix): return f"{prefix}{i}" -# --- Omni model builder (for Omni -> OSI -> Omni) --------------------------------- +# --- Omni model builder (for Omni -> Ossie -> Omni) --------------------------------- def _maybe_meta(rnd, target): if rnd.chance(0.4): @@ -252,9 +252,9 @@ def build_omni(rnd): return files -# --- OSI model builder (for OSI -> Omni -> OSI) ----------------------------------- +# --- Ossie model builder (for Ossie -> Omni -> Ossie) ----------------------------------- -def _osi_field(rnd, names): +def _ossie_field(rnd, names): fname = names.next("f") flavor = rnd.count(0, 2) if flavor == 0: @@ -282,8 +282,8 @@ def _osi_field(rnd, names): return field, expr -def build_osi(rnd): - """Generate an OSI model dict in the round-trippable subset.""" +def build_ossie(rnd): + """Generate an Ossie model dict in the round-trippable subset.""" names = _Names() fact = names.next("fact") dim_names = [names.next("dim") for _ in range(rnd.count(0, 3))] @@ -295,7 +295,7 @@ def build_osi(rnd): "source": f"{rnd.colname()}.{rnd.colname()}.{rnd.colname()}"} fields, simple_fields = [], [] for _ in range(rnd.count(1, 4)): - field, expr = _osi_field(rnd, names) + field, expr = _ossie_field(rnd, names) fields.append(field) if expr == field["name"]: simple_fields.append(field["name"]) @@ -327,7 +327,7 @@ def build_osi(rnd): if flavor == 0: expr = "COUNT(*)" elif flavor == 1 and fields_of[target]: - expr = f"{rnd.pick(_OSI_AGGS)}({target}.{rnd.pick(fields_of[target])})" + expr = f"{rnd.pick(_OSSIE_AGGS)}({target}.{rnd.pick(fields_of[target])})" else: expr = f"SUM({fact}.{fields_of[fact][0]}) / COUNT(*)" metric = {"name": mname, @@ -349,7 +349,7 @@ def build_osi(rnd): model["relationships"] = relationships if metrics: model["metrics"] = metrics - return {"version": OSI_VERSION, "semantic_model": [model]} + return {"version": OSSIE_VERSION, "semantic_model": [model]} # --- Round-trip assertions ------------------------------------------------------- @@ -361,18 +361,18 @@ def _quiet(fn, *args, **kwargs): def assert_omni_roundtrip(files): - """An Omni model survives Omni -> OSI -> Omni with every file identical + """An Omni model survives Omni -> Ossie -> Omni with every file identical (structurally -- YAML key order and formatting aside).""" - osi = _quiet(convert_omni_to_osi, files) - files2 = _quiet(convert_osi_to_omni, osi) + ossie = _quiet(convert_omni_to_ossie, files) + files2 = _quiet(convert_ossie_to_omni, ossie) parsed1 = {name: load_yaml(text, name) for name, text in files.items()} parsed2 = {name: load_yaml(text, name) for name, text in files2.items()} assert parsed2 == parsed1 -def assert_osi_roundtrip(osi): - """An OSI model dict survives OSI -> Omni -> OSI up to the documented +def assert_ossie_roundtrip(ossie): + """An Ossie model dict survives Ossie -> Omni -> Ossie up to the documented normalizations (see _util.strip_normalized).""" - files = _quiet(convert_osi_to_omni, dump_yaml(osi)) - osi2 = load_yaml(_quiet(convert_omni_to_osi, files)) - assert strip_normalized(osi2) == strip_normalized(osi) + files = _quiet(convert_ossie_to_omni, dump_yaml(ossie)) + ossie2 = load_yaml(_quiet(convert_omni_to_ossie, files)) + assert strip_normalized(ossie2) == strip_normalized(ossie) diff --git a/converters/omni/tests/_util.py b/converters/omni/tests/_util.py index b1d72acb..2e1418a4 100644 --- a/converters/omni/tests/_util.py +++ b/converters/omni/tests/_util.py @@ -21,7 +21,7 @@ import json import pathlib -from osi_omni._common import load_yaml # src is on sys.path via conftest.py +from ossie_omni._common import load_yaml # src is on sys.path via conftest.py FIXTURES = pathlib.Path(__file__).resolve().parent / "fixtures" REPO_ROOT = pathlib.Path(__file__).resolve().parents[3] @@ -71,8 +71,8 @@ def walk(node): return obj -def strip_normalized(osi): - """Normalize away what the OSI -> Omni -> OSI trip changes by design, so a +def strip_normalized(ossie): + """Normalize away what the Ossie -> Omni -> Ossie trip changes by design, so a round-trip comparison reflects the documented behavior: - `custom_extensions` everywhere: the import adds OMNI stashes (topic set, @@ -87,8 +87,8 @@ def strip_normalized(osi): - a primary-key column no field covers materializes as a hidden dimension on export, so it comes back as an extra (stash-only) field. """ - osi = copy.deepcopy(osi) - for model in osi.get("semantic_model", []): + ossie = copy.deepcopy(ossie) + for model in ossie.get("semantic_model", []): model.pop("custom_extensions", None) ai = model.get("ai_context") if isinstance(ai, dict): @@ -142,4 +142,4 @@ def strip_normalized(osi): # their measure lives on. if model.get("metrics"): model["metrics"].sort(key=lambda m: m["name"]) - return osi + return ossie diff --git a/converters/omni/tests/fixtures/fixtureA_osi.yaml b/converters/omni/tests/fixtures/fixtureA_ossie.yaml similarity index 93% rename from converters/omni/tests/fixtures/fixtureA_osi.yaml rename to converters/omni/tests/fixtures/fixtureA_ossie.yaml index 0cb830fd..5800db47 100644 --- a/converters/omni/tests/fixtures/fixtureA_osi.yaml +++ b/converters/omni/tests/fixtures/fixtureA_ossie.yaml @@ -15,11 +15,11 @@ # specific language governing permissions and limitations # under the License. -# yaml-language-server: $schema=../../../../core-spec/osi-schema.json +# yaml-language-server: $schema=../../../../core-spec/ossie-schema.json # -# Fixture A -- all-native round-trip (OSI -> Omni -> OSI). +# Fixture A -- all-native round-trip (Ossie -> Omni -> Ossie). # Star schema; every construct maps to a native Omni slot. Documented -# normalizations on the OSI -> Omni -> OSI trip: relationship names are +# normalizations on the Ossie -> Omni -> Ossie trip: relationship names are # regenerated (`_to_`), and unique_keys that merely restate the # primary key are absorbed by it. diff --git a/converters/omni/tests/fixtures/fixtureB_omni/model.yaml b/converters/omni/tests/fixtures/fixtureB_omni/model.yaml index b9602ec6..5b809973 100644 --- a/converters/omni/tests/fixtures/fixtureB_omni/model.yaml +++ b/converters/omni/tests/fixtures/fixtureB_omni/model.yaml @@ -15,8 +15,8 @@ # specific language governing permissions and limitations # under the License. -# Fixture B -- stash round-trip (Omni -> OSI -> Omni, lossless). -# The model file has no OSI home and round-trips via the model-level stash. +# Fixture B -- stash round-trip (Omni -> Ossie -> Omni, lossless). +# The model file has no Ossie home and round-trips via the model-level stash. included_schemas: - ecomm week_start_day: Sunday diff --git a/converters/omni/tests/fixtures/fixtureB_omni/relationships.yaml b/converters/omni/tests/fixtures/fixtureB_omni/relationships.yaml index 6dbc77ef..9dd6da6d 100644 --- a/converters/omni/tests/fixtures/fixtureB_omni/relationships.yaml +++ b/converters/omni/tests/fixtures/fixtureB_omni/relationships.yaml @@ -16,7 +16,7 @@ # under the License. # Exercises: non-default join_type, reversible, a composite-key one_to_many -# join (flipped to OSI's many-side-first orientation and flipped back on export). +# join (flipped to Ossie's many-side-first orientation and flipped back on export). - join_from_view: order_items join_to_view: users on_sql: ${order_items.user_id} = ${users.id} diff --git a/converters/omni/tests/fixtures/fixtureB_omni/topics/order_analysis.topic.yaml b/converters/omni/tests/fixtures/fixtureB_omni/topics/order_analysis.topic.yaml index 9107f42e..1e7c02dc 100644 --- a/converters/omni/tests/fixtures/fixtureB_omni/topics/order_analysis.topic.yaml +++ b/converters/omni/tests/fixtures/fixtureB_omni/topics/order_analysis.topic.yaml @@ -15,7 +15,7 @@ # specific language governing permissions and limitations # under the License. -# Topic curation (label, joins, default_filters, fields) has no OSI home and +# Topic curation (label, joins, default_filters, fields) has no Ossie home and # round-trips via the model-level stash; description/ai_context map natively. base_view: order_items label: Order Analysis diff --git a/converters/omni/tests/test_omni_to_osi.py b/converters/omni/tests/test_omni_to_ossie.py similarity index 97% rename from converters/omni/tests/test_omni_to_osi.py rename to converters/omni/tests/test_omni_to_ossie.py index 704d5443..c2795246 100644 --- a/converters/omni/tests/test_omni_to_osi.py +++ b/converters/omni/tests/test_omni_to_ossie.py @@ -15,22 +15,22 @@ # specific language governing permissions and limitations # under the License. -"""Unit tests for the Omni -> OSI importer.""" +"""Unit tests for the Omni -> Ossie importer.""" import json import warnings import pytest -from osi_omni import ConversionError, convert_omni_to_osi -from osi_omni._common import dump_yaml +from ossie_omni import ConversionError, convert_omni_to_ossie +from ossie_omni._common import dump_yaml from _util import load_fixture_dir, parse def imp(files, **kwargs): with warnings.catch_warnings(): warnings.simplefilter("ignore") - return parse(convert_omni_to_osi(files, **kwargs))["semantic_model"][0] + return parse(convert_omni_to_ossie(files, **kwargs))["semantic_model"][0] def minimal_files(**view_overrides): @@ -219,7 +219,7 @@ def test_field_reference_in_on_sql_resolves_to_column(): def test_non_equi_join_is_stashed_not_converted(): - # A range join is valid in Omni but has no OSI relationship form; it is + # A range join is valid in Omni but has no Ossie relationship form; it is # preserved verbatim (with its position) rather than failing the import. entry = {"join_from_view": "orders", "join_to_view": "users", "on_sql": "${orders.total} >= ${users.threshold}", @@ -381,7 +381,7 @@ def test_query_view_preserved_as_extra_file(): files["views/facts.query.view.yaml"] = "schema: s\nsql: SELECT 1\n" with warnings.catch_warnings(record=True) as ws: warnings.simplefilter("always") - model = parse(convert_omni_to_osi(files))["semantic_model"][0] + model = parse(convert_omni_to_ossie(files))["semantic_model"][0] assert any("query views" in str(w.message) for w in ws) assert "views/facts.query.view.yaml" in stash_of(model)["extra_files"] diff --git a/converters/omni/tests/test_osi_to_omni.py b/converters/omni/tests/test_ossie_to_omni.py similarity index 91% rename from converters/omni/tests/test_osi_to_omni.py rename to converters/omni/tests/test_ossie_to_omni.py index e6d1d0a7..d6de14b1 100644 --- a/converters/omni/tests/test_osi_to_omni.py +++ b/converters/omni/tests/test_ossie_to_omni.py @@ -15,21 +15,21 @@ # specific language governing permissions and limitations # under the License. -"""Unit tests for the OSI -> Omni exporter.""" +"""Unit tests for the Ossie -> Omni exporter.""" import warnings import pytest -from osi_omni import ConversionError, convert_osi_to_omni -from osi_omni._common import dump_yaml +from ossie_omni import ConversionError, convert_ossie_to_omni +from ossie_omni._common import dump_yaml from _util import REPO_ROOT, load_fixture, load_fixture_dir, parse, parse_files -def export(osi_yaml, **kwargs): +def export(ossie_yaml, **kwargs): with warnings.catch_warnings(): warnings.simplefilter("ignore") - return convert_osi_to_omni(osi_yaml, **kwargs) + return convert_ossie_to_omni(ossie_yaml, **kwargs) def minimal(**model_overrides): @@ -53,7 +53,7 @@ def _field(name, expr=None, dialect="ANSI_SQL", **extra): # --- fixtures --------------------------------------------------------------- def test_fixtureA_export_matches_expected(): - files = export(load_fixture("fixtureA_osi.yaml")) + files = export(load_fixture("fixtureA_ossie.yaml")) assert parse_files(files) == parse_files(load_fixture_dir("fixtureA_omni")) @@ -178,7 +178,7 @@ def test_composite_relationship_joins_with_and(): def test_topic_generated_with_fk_sink_base_and_join_tree(): - files = export(load_fixture("fixtureA_osi.yaml")) + files = export(load_fixture("fixtureA_ossie.yaml")) topic = parse(files["topics/sales.topic.yaml"]) assert topic["base_view"] == "orders" assert topic["joins"] == {"customer": {}} @@ -187,7 +187,7 @@ def test_topic_generated_with_fk_sink_base_and_join_tree(): def test_explicit_base_view_overrides_heuristic(): - files = export(load_fixture("fixtureA_osi.yaml"), base_view="customer") + files = export(load_fixture("fixtureA_ossie.yaml"), base_view="customer") topic = parse(files["topics/sales.topic.yaml"]) assert topic["base_view"] == "customer" assert topic["joins"] == {"orders": {}} @@ -199,10 +199,10 @@ def test_unknown_base_view_rejected(): def test_multiple_roots_require_base_view(): - osi = minimal(datasets=[{"name": "a", "source": "db.s.a"}, + ossie = minimal(datasets=[{"name": "a", "source": "db.s.a"}, {"name": "b", "source": "db.s.b"}]) with pytest.raises(ConversionError, match="--base-view"): - export(osi) + export(ossie) def test_metric_aggregate_becomes_structured_measure(): @@ -262,10 +262,10 @@ def test_names_are_sanitized(): def test_sanitization_collision_rejected(): - osi = minimal(datasets=[{"name": "Orders", "source": "db.s.a"}, + ossie = minimal(datasets=[{"name": "Orders", "source": "db.s.a"}, {"name": "orders", "source": "db.s.b"}]) with pytest.raises(ConversionError, match="collides"): - export(osi) + export(ossie) def test_duplicate_metric_names_rejected(): @@ -276,32 +276,32 @@ def test_duplicate_metric_names_rejected(): def test_relationship_column_count_mismatch_rejected(): - osi = minimal(datasets=[{"name": "a", "source": "db.s.a"}, + ossie = minimal(datasets=[{"name": "a", "source": "db.s.a"}, {"name": "b", "source": "db.s.b"}], relationships=[{"name": "r", "from": "a", "to": "b", "from_columns": ["x"], "to_columns": ["y", "z"]}]) with pytest.raises(ConversionError, match="same length"): - export(osi) + export(ossie) def test_unknown_relationship_dataset_rejected(): - osi = minimal(relationships=[{"name": "r", "from": "orders", "to": "ghost", + ossie = minimal(relationships=[{"name": "r", "from": "orders", "to": "ghost", "from_columns": ["x"], "to_columns": ["y"]}]) with pytest.raises(ConversionError, match="unknown dataset"): - export(osi) + export(ossie) def test_unsupported_version_rejected(): - with pytest.raises(ConversionError, match="Unsupported OSI version"): + with pytest.raises(ConversionError, match="Unsupported Ossie version"): export(dump_yaml({"version": "9.9.9", "semantic_model": [{"name": "m"}]})) # --- warnings --------------------------------------------------------------- -def _warnings_of(osi_yaml, **kwargs): +def _warnings_of(ossie_yaml, **kwargs): with warnings.catch_warnings(record=True) as ws: warnings.simplefilter("always") - convert_osi_to_omni(osi_yaml, **kwargs) + convert_ossie_to_omni(ossie_yaml, **kwargs) return [str(w.message) for w in ws] @@ -320,12 +320,12 @@ def test_unique_keys_restating_primary_key_do_not_warn(): def test_field_without_usable_dialect_warns_and_drops(): - osi = minimal(datasets=[ + ossie = minimal(datasets=[ {"name": "orders", "source": "db.s.t", "fields": [_field("x", dialect="MDX")]}]) - msgs = _warnings_of(osi) + msgs = _warnings_of(ossie) assert any("no usable" in m for m in msgs) - assert "dimensions" not in parse(export(osi)["views/orders.view.yaml"]) + assert "dimensions" not in parse(export(ossie)["views/orders.view.yaml"]) def test_relationship_ai_context_warns(): @@ -344,8 +344,8 @@ def test_foreign_vendor_extensions_warn(): def test_multiple_models_warn_and_first_converts(): - osi = parse(minimal()) - osi["semantic_model"].append({"name": "second", "datasets": [ + ossie = parse(minimal()) + ossie["semantic_model"].append({"name": "second", "datasets": [ {"name": "x", "source": "db.s.x"}]}) - msgs = _warnings_of(dump_yaml(osi)) + msgs = _warnings_of(dump_yaml(ossie)) assert any("multiple semantic models" in m for m in msgs) diff --git a/converters/omni/tests/test_real_world_layout.py b/converters/omni/tests/test_real_world_layout.py index c78864dd..d170437a 100644 --- a/converters/omni/tests/test_real_world_layout.py +++ b/converters/omni/tests/test_real_world_layout.py @@ -30,7 +30,7 @@ metadata, `timeframes: []`, camelCase/underscore-edged identifiers, and schema names with spaces all occur. -Every one of these was found in production models; the round trip through OSI +Every one of these was found in production models; the round trip through Ossie must reproduce the original files exactly (same paths, same parsed content). """ @@ -38,8 +38,8 @@ import pytest -from osi_omni import ConversionError, convert_omni_to_osi, convert_osi_to_omni -from osi_omni._common import dump_yaml, load_yaml +from ossie_omni import ConversionError, convert_omni_to_ossie, convert_ossie_to_omni +from ossie_omni._common import dump_yaml, load_yaml REAL_FILES = { @@ -51,7 +51,7 @@ "join_type": "always_left", "on_sql": "${delighted__response.person_id} = ${delighted__person.id}", "relationship_type": "assumed_many_to_one"}, - # Non-equi (range) join: valid Omni, no OSI form -- stashed. + # Non-equi (range) join: valid Omni, no Ossie form -- stashed. {"join_from_view": "delighted__response", "join_to_view": "github__team_membership", "join_type": "always_left", @@ -65,7 +65,7 @@ "on_sql": "${github__team_membership.user_id} = ${membership_owner.id} " "and ${github__team_membership.org} = ${membership_owner.org}", "relationship_type": "many_to_one"}, - # Second join between the same pair (name dedup on the OSI side). + # Second join between the same pair (name dedup on the Ossie side). {"join_from_view": "github__team_membership", "join_to_view": "delighted__person", "on_sql": "${github__team_membership.approver_id} = " @@ -128,8 +128,8 @@ def _roundtrip(files): with warnings.catch_warnings(): warnings.simplefilter("ignore") - osi_yaml = convert_omni_to_osi(files) - return osi_yaml, convert_osi_to_omni(osi_yaml) + ossie_yaml = convert_omni_to_ossie(files) + return ossie_yaml, convert_ossie_to_omni(ossie_yaml) def test_api_layout_roundtrip_is_lossless(): @@ -144,35 +144,35 @@ def test_api_layout_roundtrip_is_lossless(): def test_qualified_view_names_come_from_reference_comment(): with warnings.catch_warnings(): warnings.simplefilter("ignore") - osi = load_yaml(convert_omni_to_osi(REAL_FILES))["semantic_model"][0] - names = {d["name"] for d in osi["datasets"]} + ossie = load_yaml(convert_omni_to_ossie(REAL_FILES))["semantic_model"][0] + names = {d["name"] for d in ossie["datasets"]} assert "delighted__response" in names assert "upload" in names # comment name wins even with a folder # The qualified name resolves relationship references and same-view # ${view.field} compound-key entries. - rel = osi["relationships"][0] + rel = ossie["relationships"][0] assert rel["from"] == "delighted__response" assert rel["to"] == "delighted__person" - response = next(d for d in osi["datasets"] + response = next(d for d in ossie["datasets"] if d["name"] == "delighted__response") assert response["primary_key"] == ["id", "org"] assert response["source"] == "DELIGHTED.RESPONSE" - upload = next(d for d in osi["datasets"] if d["name"] == "upload") + upload = next(d for d in ossie["datasets"] if d["name"] == "upload") assert upload["source"] == '"Omni Views".upload' -def test_unmappable_omni_features_drop_out_of_osi_but_survive(): +def test_unmappable_omni_features_drop_out_of_ossie_but_survive(): with warnings.catch_warnings(): warnings.simplefilter("ignore") - osi = load_yaml(convert_omni_to_osi(REAL_FILES))["semantic_model"][0] + ossie = load_yaml(convert_omni_to_ossie(REAL_FILES))["semantic_model"][0] # The extends view is not a dataset; the templated dimension not a field. - assert "delighted__response_ext" not in {d["name"] for d in osi["datasets"]} - response = next(d for d in osi["datasets"] + assert "delighted__response_ext" not in {d["name"] for d in ossie["datasets"]} + response = next(d for d in ossie["datasets"] if d["name"] == "delighted__response") assert "templated" not in {f["name"] for f in response["fields"]} - # The non-equi join is not an OSI relationship; the two same-pair joins get - # unique OSI names. - names = [r["name"] for r in osi["relationships"]] + # The non-equi join is not an Ossie relationship; the two same-pair joins get + # unique Ossie names. + names = [r["name"] for r in ossie["relationships"]] assert len(names) == len(set(names)) == 3 @@ -184,4 +184,4 @@ def test_duplicate_canonical_view_names_rejected(): with pytest.raises(ConversionError, match="two view files"): with warnings.catch_warnings(): warnings.simplefilter("ignore") - convert_omni_to_osi(files) + convert_omni_to_ossie(files) diff --git a/converters/omni/tests/test_roundtrip.py b/converters/omni/tests/test_roundtrip.py index 6c4f5dd0..94154b3c 100644 --- a/converters/omni/tests/test_roundtrip.py +++ b/converters/omni/tests/test_roundtrip.py @@ -17,10 +17,10 @@ """Fixture-based round-trip tests. -- Omni -> OSI -> Omni must be lossless (the stash carries everything). -- OSI -> Omni -> OSI must be identical up to the documented normalizations +- Omni -> Ossie -> Omni must be lossless (the stash carries everything). +- Ossie -> Omni -> Ossie must be identical up to the documented normalizations (see _util.strip_normalized). -- Every OSI document the importer emits must validate against the core-spec +- Every Ossie document the importer emits must validate against the core-spec JSON schema (skipped when jsonschema is not installed). """ @@ -28,7 +28,7 @@ import pytest -from osi_omni import convert_omni_to_osi, convert_osi_to_omni +from ossie_omni import convert_omni_to_ossie, convert_ossie_to_omni from _util import ( REPO_ROOT, load_fixture, @@ -48,35 +48,35 @@ def _quiet(fn, *args, **kwargs): @pytest.mark.parametrize("fixture", ["fixtureA_omni", "fixtureB_omni", "tpcds_omni"]) def test_omni_roundtrip_is_lossless(fixture): files = load_fixture_dir(fixture) - osi = _quiet(convert_omni_to_osi, files) - files2 = _quiet(convert_osi_to_omni, osi) + ossie = _quiet(convert_omni_to_ossie, files) + files2 = _quiet(convert_ossie_to_omni, ossie) assert parse_files(files2) == parse_files(files) @pytest.mark.parametrize("path", [ - "fixtureA_osi.yaml", + "fixtureA_ossie.yaml", str(REPO_ROOT / "examples" / "tpcds_semantic_model.yaml"), ]) -def test_osi_roundtrip_up_to_documented_normalizations(path): +def test_ossie_roundtrip_up_to_documented_normalizations(path): if "/" in path: with open(path) as fh: - osi_yaml = fh.read() + ossie_yaml = fh.read() else: - osi_yaml = load_fixture(path) - files = _quiet(convert_osi_to_omni, osi_yaml) - osi2 = _quiet(convert_omni_to_osi, files) - original = strip_normalized(parse(osi_yaml)) - # Foreign-vendor extensions are dropped on export (documented); OSI-side + ossie_yaml = load_fixture(path) + files = _quiet(convert_ossie_to_omni, ossie_yaml) + ossie2 = _quiet(convert_omni_to_ossie, files) + original = strip_normalized(parse(ossie_yaml)) + # Foreign-vendor extensions are dropped on export (documented); Ossie-side # normalization strips all custom_extensions from both sides. - assert strip_normalized(parse(osi2)) == original + assert strip_normalized(parse(ossie2)) == original @pytest.mark.parametrize("fixture", ["fixtureA_omni", "fixtureB_omni", "tpcds_omni"]) -def test_imported_osi_validates_against_core_spec_schema(fixture): +def test_imported_ossie_validates_against_core_spec_schema(fixture): jsonschema = pytest.importorskip("jsonschema") import json - with open(REPO_ROOT / "core-spec" / "osi-schema.json") as fh: + with open(REPO_ROOT / "core-spec" / "ossie-schema.json") as fh: schema = json.load(fh) - osi = _quiet(convert_omni_to_osi, load_fixture_dir(fixture)) - jsonschema.validate(parse(osi), schema) + ossie = _quiet(convert_omni_to_ossie, load_fixture_dir(fixture)) + jsonschema.validate(parse(ossie), schema) diff --git a/converters/omni/tests/test_roundtrip_properties.py b/converters/omni/tests/test_roundtrip_properties.py index 4d2973d6..a9338f91 100644 --- a/converters/omni/tests/test_roundtrip_properties.py +++ b/converters/omni/tests/test_roundtrip_properties.py @@ -29,9 +29,9 @@ from _roundtrip_helpers import ( RandomRnd, assert_omni_roundtrip, - assert_osi_roundtrip, + assert_ossie_roundtrip, build_omni, - build_osi, + build_ossie, ) try: @@ -86,8 +86,8 @@ def test_omni_roundtrip_property(data): @given(st.data()) @settings(max_examples=50, deadline=None) - def test_osi_roundtrip_property(data): - assert_osi_roundtrip(build_osi(_HypRnd(data))) + def test_ossie_roundtrip_property(data): + assert_ossie_roundtrip(build_ossie(_HypRnd(data))) else: # pragma: no cover - exercised only without hypothesis @@ -96,5 +96,5 @@ def test_omni_roundtrip_seeded(seed): assert_omni_roundtrip(build_omni(RandomRnd(seed))) @pytest.mark.parametrize("seed", range(50)) - def test_osi_roundtrip_seeded(seed): - assert_osi_roundtrip(build_osi(RandomRnd(seed))) + def test_ossie_roundtrip_seeded(seed): + assert_ossie_roundtrip(build_ossie(RandomRnd(seed))) diff --git a/converters/omni/uv.lock b/converters/omni/uv.lock index 5a3af714..932598d6 100644 --- a/converters/omni/uv.lock +++ b/converters/omni/uv.lock @@ -102,7 +102,7 @@ wheels = [ ] [[package]] -name = "osi-omni" +name = "ossie-omni" version = "0.2.0.dev0" source = { editable = "." } dependencies = [ diff --git a/converters/orionbelt/README.md b/converters/orionbelt/README.md index b9db4fdf..122f1f63 100644 --- a/converters/orionbelt/README.md +++ b/converters/orionbelt/README.md @@ -25,10 +25,10 @@ the open standard for portable semantic models (metrics, dimensions, relationships). This package is licensed under **Apache-2.0** and may be used freely. It is the -OrionBelt converter in the OSI converter ecosystem. The canonical source is +OrionBelt converter in the Ossie converter ecosystem. The canonical source is developed in the [orionbelt-semantic-layer](https://github.com/ralfbecher/orionbelt-semantic-layer) -repository (under `packages/osi-orionbelt`) and published to PyPI from there; +repository (under `packages/ossie-orionbelt`) and published to PyPI from there; file issues and contributions upstream. ## Requirements @@ -58,81 +58,81 @@ A single `ossie-orionbelt` command with two subcommands (mirroring `ossie-dbt`): | Subcommand | Direction | In | Out | |---------|-----------|----|----| -| `obml-to-osi` | OBML -> OSI core-spec | OBML YAML | OSI YAML | -| `obml-to-osi --ontology` | OBML -> OSI ontology | OBML YAML | OSI ontology YAML | -| `osi-to-obml` | OSI core-spec -> OBML | OSI YAML | OBML YAML | +| `obml-to-ossie` | OBML -> Ossie core-spec | OBML YAML | Ossie YAML | +| `obml-to-ossie --ontology` | OBML -> Ossie ontology | OBML YAML | Ossie ontology YAML | +| `ossie-to-obml` | Ossie core-spec -> OBML | Ossie YAML | OBML YAML | ```bash -ossie-orionbelt obml-to-osi -i model.obml.yaml -o model.osi.yaml -ossie-orionbelt obml-to-osi --ontology -i model.obml.yaml -o model.ontology.yaml -ossie-orionbelt osi-to-obml -i model.osi.yaml -o model.obml.yaml +ossie-orionbelt obml-to-ossie -i model.obml.yaml -o model.ossie.yaml +ossie-orionbelt obml-to-ossie --ontology -i model.obml.yaml -o model.ontology.yaml +ossie-orionbelt ossie-to-obml -i model.ossie.yaml -o model.obml.yaml ``` `-i/--input` and `-o/--output` are required. Each subcommand prints conversion warnings and a validation summary to stderr, and exits non-zero when the produced document fails schema validation (unless `--no-validate`). Run -`ossie-orionbelt --help` or `ossie-orionbelt obml-to-osi --help` for the full +`ossie-orionbelt --help` or `ossie-orionbelt obml-to-ossie --help` for the full option list. ## Python API ```python import yaml -from ossie_orionbelt import OBMLtoOSI, OSItoOBML, validate_osi +from ossie_orionbelt import OBMLtoOssie, OssietoOBML, validate_ossie obml = yaml.safe_load(open("model.obml.yaml")) -osi = OBMLtoOSI(obml, "sales", "Sales model").convert() -result = validate_osi(osi) +ossie = OBMLtoOssie(obml, "sales", "Sales model").convert() +result = validate_ossie(ossie) assert result.valid -obml_again = OSItoOBML(osi).convert() +obml_again = OssietoOBML(ossie).convert() ``` ## Vendor extensions -OSI `custom_extensions` carry vendor-tagged payloads. This converter: +Ossie `custom_extensions` carry vendor-tagged payloads. This converter: - emits OrionBelt/OBML-proprietary data under the **`ORIONBELT`** vendor on OBML - to OSI (OBML-only filters, settings, owner, refresh, type info, etc.); -- stashes OSI-native fields that OBML can't represent (unique keys, field - labels, leftover `ai_context`) under the **`OSI`** vendor when going OSI to - OBML, restoring them to first-class OSI fields on the way back; + to Ossie (OBML-only filters, settings, owner, refresh, type info, etc.); +- stashes Ossie-native fields that OBML can't represent (unique keys, field + labels, leftover `ai_context`) under the **`Ossie`** vendor when going Ossie to + OBML, restoring them to first-class Ossie fields on the way back; - **preserves third-party vendor extensions verbatim** (e.g. `SNOWFLAKE`, `DBT`, `SALESFORCE`, `GOODDATA`) at the model, dataset, field, and - measure/metric levels, so a full OSI to OBML to OSI roundtrip keeps the - original vendor and data. OSI has no separate dimension entity, so an OBML - dimension's foreign extensions surface on its OSI field. + measure/metric levels, so a full Ossie to OBML to Ossie roundtrip keeps the + original vendor and data. Ossie has no separate dimension entity, so an OBML + dimension's foreign extensions surface on its Ossie field. Legacy `COMMON` / `OBSL` tags from earlier converter versions are still accepted on read. ## Limitations / unsupported constructs -Some OBML constructs have no native OSI equivalent and are carried in vendor +Some OBML constructs have no native Ossie equivalent and are carried in vendor `custom_extensions` (`obml_*` payloads) so they round-trip without loss back to -OBML, but are not interpreted by other OSI consumers: +OBML, but are not interpreted by other Ossie consumers: - **Many-to-many joins** - represented in OBML join cardinality; flagged on export. - **Named secondary join paths** - OBML's multiple join paths between the same pair of objects are an OBML-specific topology feature. - **Measures / metrics and column-level value concepts in the ontology layer** - - not represented in the OSI ontology export. -- **OSI metrics with no OBML representation** - a metric whose only expression is + not represented in the Ossie ontology export. +- **Ossie metrics with no OBML representation** - a metric whose only expression is in a non-SQL dialect (`MDX`, `TABLEAU`, `MAQL`), or whose SQL expression cannot - be decomposed into OBML measures/metrics, is **not** dropped: the original OSI - metric is preserved verbatim in a model-level `OSI`-vendor `custom_extension` - (`obml_unconverted_metrics`) and re-emitted on OBML to OSI, so the OSI to OBML - to OSI roundtrip stays lossless. A `LOSSY:` warning is raised for each such + be decomposed into OBML measures/metrics, is **not** dropped: the original Ossie + metric is preserved verbatim in a model-level `Ossie`-vendor `custom_extension` + (`obml_unconverted_metrics`) and re-emitted on OBML to Ossie, so the Ossie to OBML + to Ossie roundtrip stays lossless. A `LOSSY:` warning is raised for each such metric because it is **not queryable through OBML**. SQL expressions in the `ANSI_SQL`, `SNOWFLAKE`, and `DATABRICKS` dialects are all read on import. -OSI v0.1.x inputs are accepted on read via a legacy normalization shim; output -targets OSI **v0.2.0.dev0**. +Ossie v0.1.x inputs are accepted on read via a legacy normalization shim; output +targets Ossie **v0.2.0.dev0**. -See [`osi_obml_mapping_analysis.md`](./osi_obml_mapping_analysis.md) for the -full OBML <-> OSI core-spec mapping and -[`osi_obml_ontology_mapping_analysis.md`](./osi_obml_ontology_mapping_analysis.md) +See [`ossie_obml_mapping_analysis.md`](./ossie_obml_mapping_analysis.md) for the +full OBML <-> Ossie core-spec mapping and +[`ossie_obml_ontology_mapping_analysis.md`](./ossie_obml_ontology_mapping_analysis.md) for the ontology-layer mapping and its documented gaps. ## Development diff --git a/converters/orionbelt/osi_obml_mapping_analysis.md b/converters/orionbelt/ossie_obml_mapping_analysis.md similarity index 66% rename from converters/orionbelt/osi_obml_mapping_analysis.md rename to converters/orionbelt/ossie_obml_mapping_analysis.md index cb20a973..322f72e9 100644 --- a/converters/orionbelt/osi_obml_mapping_analysis.md +++ b/converters/orionbelt/ossie_obml_mapping_analysis.md @@ -17,13 +17,13 @@ under the License. --> -# OSI ↔ OBML Mapping Analysis +# Ossie ↔ OBML Mapping Analysis -> Bidirectional conversion between [Ossie](https://ossie.apache.org/) v0.2.0.dev0 and [OrionBelt ML (OBML)](https://github.com/ralfbecher/orionbelt-semantic-layer) v1.0 semantic model formats. OSI v0.1.x inputs are still accepted on read via a legacy normalization shim; output targets v0.2.0.dev0. +> Bidirectional conversion between [Ossie](https://ossie.apache.org/) v0.2.0.dev0 and [OrionBelt ML (OBML)](https://github.com/ralfbecher/orionbelt-semantic-layer) v1.0 semantic model formats. Ossie v0.1.x inputs are still accepted on read via a legacy normalization shim; output targets v0.2.0.dev0. ## 1. Structural Comparison -| Aspect | OSI v0.2.0.dev0 | OBML v1.0 | +| Aspect | Ossie v0.2.0.dev0 | OBML v1.0 | |---|---|---| | **Top-level** | `semantic_model[]` (array of models) | Single model with `dataObjects`, `dimensions`, `measures`, `metrics` sections | | **Tables / Entities** | `datasets[]` (flat array) | `dataObjects{}` (named dictionary) | @@ -33,7 +33,7 @@ | **Dimensions** | `field.dimension.is_time` (inline flag on fields) | `dimensions{}` (separate top-level section) | | **Measures** | N/A (merged into metrics) | `measures{}` (explicit aggregation definitions) | | **Metrics** | `metrics[]` with full SQL expressions | `metrics{}` with `{[Measure]}` references | -| **AI Context** | `ai_context` on every entity | `customExtensions` with `vendor: "OSI"` | +| **AI Context** | `ai_context` on every entity | `customExtensions` with `vendor: "Ossie"` | | **Extensibility** | `custom_extensions[]` per entity | `customExtensions[]` per entity | | **Keys** | `primary_key`, `unique_keys` on datasets | N/A | | **Secondary joins** | N/A | `secondary: true`, `pathName` | @@ -43,14 +43,14 @@ ### 2.1 Naming Convention -- **OSI** uses snake_case codes everywhere (`name: "store_sales"`) +- **Ossie** uses snake_case codes everywhere (`name: "store_sales"`) - **OBML** supports dual naming — a display name as the dictionary key and a `code` for the physical SQL reference -During OSI → OBML conversion, field names are used directly as both the display name and code. During OBML → OSI conversion, the `code` value becomes the OSI field `name`. +During Ossie → OBML conversion, field names are used directly as both the display name and code. During OBML → Ossie conversion, the `code` value becomes the Ossie field `name`. ### 2.2 Relationship Placement -- **OSI** defines relationships globally, referencing dataset names by string +- **Ossie** defines relationships globally, referencing dataset names by string - **OBML** defines joins inline on the "from" side data object The converter restructures between these two representations automatically, preserving column mappings and generating descriptive relationship names. @@ -59,23 +59,23 @@ The converter restructures between these two representations automatically, pres This is the most fundamental structural difference between the two formats. -- **OSI** has a single "metrics" concept with full SQL expressions (e.g., `SUM(store_sales.ss_ext_sales_price)`) +- **Ossie** has a single "metrics" concept with full SQL expressions (e.g., `SUM(store_sales.ss_ext_sales_price)`) - **OBML** explicitly separates: - **Measures**: Simple aggregations on columns (e.g., `SUM` of `ss_ext_sales_price`) - **Metrics**: Derived calculations referencing measures via `{[Name]}` syntax (e.g., `{[total_sales]} / {[customer_count]}`) The converter handles this decomposition automatically: -| OSI metric type | OBML mapping | +| Ossie metric type | OBML mapping | |---|---| | `AGG(dataset.column)` | Direct measure | | `AGG(DISTINCT dataset.column)` | Measure with `distinct: true` | | `AGG(expr)` (e.g., `SUM(a.x * a.y)`) | Expression-based measure | | Multi-aggregation expression | Auto-generated measures + metric formula | -**Example** — OSI metric `customer_lifetime_value`: +**Example** — Ossie metric `customer_lifetime_value`: ```yaml -# OSI +# Ossie expression: SUM(store_sales.ss_ext_sales_price) / COUNT(DISTINCT customer.c_customer_sk) ``` @@ -104,14 +104,14 @@ metrics: expression: "{[total_sales]} / {[_customer_c_customer_sk_count_distinct]}" ``` -When a simple OSI metric (e.g., `SUM(store_sales.ss_ext_sales_price)`) is equivalent to an existing named measure, the converter deduplicates and reuses the named measure rather than creating a redundant auto-measure. +When a simple Ossie metric (e.g., `SUM(store_sales.ss_ext_sales_price)`) is equivalent to an existing named measure, the converter deduplicates and reuses the named measure rather than creating a redundant auto-measure. ### 2.4 AI Context Preservation -OSI's `ai_context` (instructions, synonyms, examples) is preserved losslessly during conversion via OBML's `customExtensions` mechanism: +Ossie's `ai_context` (instructions, synonyms, examples) is preserved losslessly during conversion via OBML's `customExtensions` mechanism: ```yaml -# OSI input +# Ossie input ai_context: synonyms: - "sales transactions" @@ -119,17 +119,17 @@ ai_context: # OBML output (via customExtensions) customExtensions: - - vendor: OSI + - vendor: Ossie data: '{"synonyms": ["sales transactions", "store purchases"]}' ``` This applies at all levels: datasets → data objects, fields → columns, and model-level `ai_context`. -During OBML → OSI conversion, the `customExtensions` with `vendor: "OSI"` are read back and restored as native `ai_context` on the OSI side. +During OBML → Ossie conversion, the `customExtensions` with `vendor: "Ossie"` are read back and restored as native `ai_context` on the Ossie side. -### 2.5 OBML-Specific Features (Not Representable in OSI) +### 2.5 OBML-Specific Features (Not Representable in Ossie) -These OBML features have no direct OSI equivalent. Where possible, metadata is preserved in OSI `ai_context` or `custom_extensions` (with `vendor_name: "COMMON"` and `obml_`-prefixed keys) for lossless roundtrip: +These OBML features have no direct Ossie equivalent. Where possible, metadata is preserved in Ossie `ai_context` or `custom_extensions` (with `vendor_name: "COMMON"` and `obml_`-prefixed keys) for lossless roundtrip: - Secondary joins with `pathName` (preserved in relationship `ai_context`) - `allowFanOut` — preserved in metric `custom_extensions` (`obml_allow_fan_out`) @@ -145,31 +145,31 @@ These OBML features have no direct OSI equivalent. Where possible, metadata is p - Locale settings — not yet preserved - `abstractType` (OBML type system) — preserved in field `custom_extensions` (`obml_abstract_type`) -### 2.6 OSI-Specific Features and How They Map to OBML +### 2.6 Ossie-Specific Features and How They Map to OBML -- **`primary_key`** — natively represented: OSI's dataset-level `primary_key` array maps to per-column `primaryKey: true` on OBML columns (`DataObjectColumn.primaryKey`), and back to the dataset array on export. -- **`unique_keys`** — no native OBML equivalent; round-trips via an `OSI`-vendor `customExtension` (`obml_unique_keys`). +- **`primary_key`** — natively represented: Ossie's dataset-level `primary_key` array maps to per-column `primaryKey: true` on OBML columns (`DataObjectColumn.primaryKey`), and back to the dataset array on export. +- **`unique_keys`** — no native OBML equivalent; round-trips via an `Ossie`-vendor `customExtension` (`obml_unique_keys`). - **Multi-dialect expressions** — on import the converter reads the first available SQL dialect in the order `ANSI_SQL`, `SNOWFLAKE`, `DATABRICKS`; non-SQL dialects (`MDX`, `TABLEAU`, `MAQL`) are not parsed. A metric with no SQL-parseable dialect, or an expression OBML cannot decompose, is preserved verbatim (`obml_unconverted_metrics`) with a `LOSSY:` warning rather than dropped. On export, OBML measures/metrics emit `ANSI_SQL`. - **`ai_context`** — preserved losslessly via `customExtensions` (see Section 2.4). - **`custom_extensions`** — mapped to OBML `customExtensions`. ## 3. Conversion Strategies -### 3.1 OSI → OBML +### 3.1 Ossie → OBML 1. Parse `source` string to extract `database`, `schema`, and `table` 2. Convert fields to columns with type inference (heuristic-based `abstractType`) 3. Restructure global relationships into inline joins on data objects 4. Decompose metric SQL expressions into OBML measures + metrics 5. Extract dimension-flagged fields into the top-level `dimensions` section (excluding FK/PK join keys) -6. Preserve `ai_context` losslessly via `customExtensions` (vendor: `"OSI"`) +6. Preserve `ai_context` losslessly via `customExtensions` (vendor: `"Ossie"`) -### 3.2 OBML → OSI +### 3.2 OBML → Ossie -1. Combine `database.schema.code` into the OSI `source` string +1. Combine `database.schema.code` into the Ossie `source` string 2. Convert columns to fields with `ANSI_SQL` dialect expressions 3. Extract inline joins into global relationships with generated names -4. Convert measures to OSI metrics with SQL expressions +4. Convert measures to Ossie metrics with SQL expressions 5. Expand metric templates by substituting measure SQL into `{[Name]}` references 6. Map OBML dimension metadata into `field.dimension.is_time` flags 7. Preserve secondary join info in relationship `ai_context` @@ -184,9 +184,9 @@ The converter includes dual-layer validation for both formats, ensuring that con 1. **JSON Schema** — validates against `schema/obml-schema.json` (Draft 7) 2. **Semantic** — runs OrionBelt's `ReferenceResolver` + `SemanticValidator` (reference integrity, cycle detection, multipath detection, duplicate identifiers) -### 4.2 OSI Validation +### 4.2 Ossie Validation -1. **JSON Schema** — validates against the OSI core `osi-schema.json` (Draft 2020-12), resolved from the repo's `core-spec/` (no vendored copy) +1. **JSON Schema** — validates against the Ossie core `ossie-schema.json` (Draft 2020-12), resolved from the repo's `core-spec/` (no vendored copy) 2. **Unique names** — checks uniqueness of dataset, field, metric, and relationship names 3. **References** — verifies that relationship `from`/`to` reference existing datasets @@ -199,62 +199,62 @@ Validation runs automatically after each conversion. Use `--no-validate` to skip A single `ossie-orionbelt` command with two subcommands is installed with the package: ```bash -# OSI → OBML -ossie-orionbelt osi-to-obml -i tpcds_osi.yaml -o tpcds_as_obml.yaml +# Ossie → OBML +ossie-orionbelt ossie-to-obml -i tpcds_ossie.yaml -o tpcds_as_obml.yaml -# OBML → OSI -ossie-orionbelt obml-to-osi -i tpcds_as_obml.yaml -o tpcds_obml_as_osi.yaml \ +# OBML → Ossie +ossie-orionbelt obml-to-ossie -i tpcds_as_obml.yaml -o tpcds_obml_as_ossie.yaml \ --model-name tpcds_retail_model \ --description "TPC-DS retail semantic model" -# OBML → OSI ontology document -ossie-orionbelt obml-to-osi --ontology -i tpcds_as_obml.yaml -o tpcds_ontology.yaml +# OBML → Ossie ontology document +ossie-orionbelt obml-to-ossie --ontology -i tpcds_as_obml.yaml -o tpcds_ontology.yaml # Skip validation -ossie-orionbelt osi-to-obml -i input.yaml -o output.yaml --no-validate +ossie-orionbelt ossie-to-obml -i input.yaml -o output.yaml --no-validate ``` ### CLI Options | Subcommand / Option | Description | |---|---| -| `osi-to-obml` | Convert OSI → OBML | -| `obml-to-osi` | Convert OBML → OSI | -| `--ontology` | (`obml-to-osi`) emit an OSI ontology document instead of core-spec | +| `ossie-to-obml` | Convert Ossie → OBML | +| `obml-to-ossie` | Convert OBML → Ossie | +| `--ontology` | (`obml-to-ossie`) emit an Ossie ontology document instead of core-spec | | `-i`, `--input` | Input file (required) | | `-o`, `--output` | Output file (required) | -| `--model-name` | Model name for OBML → OSI | -| `--description` | Model description for OBML → OSI | -| `--ai-instructions` | AI instructions for OBML → OSI | -| `--database` | Default database for OSI → OBML (default: `ANALYTICS`) | -| `--schema` | Default schema for OSI → OBML (default: `PUBLIC`) | +| `--model-name` | Model name for OBML → Ossie | +| `--description` | Model description for OBML → Ossie | +| `--ai-instructions` | AI instructions for OBML → Ossie | +| `--database` | Default database for Ossie → OBML (default: `ANALYTICS`) | +| `--schema` | Default schema for Ossie → OBML (default: `PUBLIC`) | | `--no-validate` | Skip post-conversion validation | ### Python API ```python -from ossie_orionbelt import OSItoOBML, OBMLtoOSI, validate_obml, validate_osi +from ossie_orionbelt import OssietoOBML, OBMLtoOssie, validate_obml, validate_ossie -# OSI → OBML -converter = OSItoOBML(osi_dict) +# Ossie → OBML +converter = OssietoOBML(ossie_dict) obml = converter.convert() result = validate_obml(obml) assert result.valid -# OBML → OSI -converter = OBMLtoOSI(obml_dict, model_name="my_model") -osi = converter.convert() -result = validate_osi(osi) +# OBML → Ossie +converter = OBMLtoOssie(obml_dict, model_name="my_model") +ossie = converter.convert() +result = validate_ossie(ossie) assert result.valid ``` ## 6. Example: TPC-DS Roundtrip -The converter is validated against the official [TPC-DS example](https://github.com/open-semantic-interchange/OSI/blob/main/examples/tpcds_semantic_model.yaml) from the OSI repository. That file is vendored at `tests/fixtures/tpcds_semantic_model.yaml` and exercised by `tests/test_osi_tpcds_baseline.py`, which runs the OSI converters guide's [conceptual conversion flow](https://github.com/open-semantic-interchange/OSI/blob/main/converters/index.md#example-conceptual-conversion-flow) end to end: OSI to OBML to OSI, asserting validity at each step and that the example's `SALESFORCE` and `DBT` custom extensions survive the round-trip (step 7). +The converter is validated against the official [TPC-DS example](https://github.com/open-semantic-interchange/Ossie/blob/main/examples/tpcds_semantic_model.yaml) from the Ossie repository. That file is vendored at `tests/fixtures/tpcds_semantic_model.yaml` and exercised by `tests/test_ossie_tpcds_baseline.py`, which runs the Ossie converters guide's [conceptual conversion flow](https://github.com/open-semantic-interchange/Ossie/blob/main/converters/index.md#example-conceptual-conversion-flow) end to end: Ossie to OBML to Ossie, asserting validity at each step and that the example's `SALESFORCE` and `DBT` custom extensions survive the round-trip (step 7). -### OSI → OBML +### Ossie → OBML -The TPC-DS OSI model with 5 datasets, 4 relationships, and 5 metrics converts cleanly to OBML: +The TPC-DS Ossie model with 5 datasets, 4 relationships, and 5 metrics converts cleanly to OBML: - 5 data objects with inline joins - 16 dimensions (FK/PK join keys excluded) @@ -262,9 +262,9 @@ The TPC-DS OSI model with 5 datasets, 4 relationships, and 5 metrics converts cl - 2 metrics (composite expressions referencing measures) - All `ai_context` synonyms preserved via `customExtensions` -### OBML → OSI (Roundtrip) +### OBML → Ossie (Roundtrip) -Converting the OBML output back to OSI produces a valid OSI model where: +Converting the OBML output back to Ossie produces a valid Ossie model where: - All `ai_context` synonyms are restored from `customExtensions` - Measures are re-expanded into SQL metric expressions @@ -275,14 +275,14 @@ Converting the OBML output back to OSI produces a valid OSI model where: | File | Description | |---|---| -| `tests/fixtures/tpcds_osi.yaml` | Official TPC-DS OSI example (from OSI repo) | +| `tests/fixtures/tpcds_ossie.yaml` | Official TPC-DS Ossie example (from Ossie repo) | | `tests/fixtures/tpcds_as_obml.yaml` | Converted OBML output | -| `core-spec/osi-schema.json` | OSI core JSON Schema (Draft 2020-12), resolved at runtime; not vendored | +| `core-spec/ossie-schema.json` | Ossie core JSON Schema (Draft 2020-12), resolved at runtime; not vendored | | `src/ossie_orionbelt/converter.py` | Bidirectional converter with validation | ## 7. Future Considerations -- **MCP/API integration** — Expose OSI import/export as OrionBelt MCP tools or REST API endpoints +- **MCP/API integration** — Expose Ossie import/export as OrionBelt MCP tools or REST API endpoints - **Multi-dialect support** — Preserve non-ANSI dialect expressions during roundtrip - **Primary keys in OBML** — Add optional `primary_key` to data objects for richer metadata -- **Vendor enum** — Register `"ORIONBELT"` as an OSI vendor for OBML-specific extensions +- **Vendor enum** — Register `"ORIONBELT"` as an Ossie vendor for OBML-specific extensions diff --git a/converters/orionbelt/osi_obml_ontology_mapping_analysis.md b/converters/orionbelt/ossie_obml_ontology_mapping_analysis.md similarity index 78% rename from converters/orionbelt/osi_obml_ontology_mapping_analysis.md rename to converters/orionbelt/ossie_obml_ontology_mapping_analysis.md index 1f3e06ca..42768b05 100644 --- a/converters/orionbelt/osi_obml_ontology_mapping_analysis.md +++ b/converters/orionbelt/ossie_obml_ontology_mapping_analysis.md @@ -17,15 +17,15 @@ under the License. --> -# OBML → OSI Ontology Mapping Analysis +# OBML → Ossie Ontology Mapping Analysis -This pins the rules used by `OBMLtoOSIOntology` to derive an **OSI ontology -document** (OSI ontology format, version `0.2.0.dev0`) from an OBML semantic +This pins the rules used by `OBMLtoOssieOntology` to derive an **Ossie ontology +document** (Ossie ontology format, version `0.2.0.dev0`) from an OBML semantic model. -The OSI ontology is a **separate document** from the OSI core-spec semantic +The Ossie ontology is a **separate document** from the Ossie core-spec semantic model (different `$id`, different required root). It is produced alongside the -core export, never merged into it — OSI's own `validation/validate.py` validates +core export, never merged into it — Ossie's own `validation/validate.py` validates one document against one schema and reads only the first YAML document, so a combined or multi-doc file is not portable. See the `include_ontology` flag on the export endpoints. @@ -49,7 +49,7 @@ ontology: # required, minItems 1 verbalizes: ["{} relates to {}"] ontology_mappings: - name: _map - semantic_model: { ...full OSI core-spec model... } # reused from OBMLtoOSI + semantic_model: { ...full Ossie core-spec model... } # reused from OBMLtoOssie concept_mappings: - concept: object_mappings: [{ expression: "." }] @@ -60,15 +60,15 @@ ontology_mappings: ## Mapping rules -| OBML construct | OSI ontology target | Notes | +| OBML construct | Ossie ontology target | Notes | |----------------|---------------------|-------| -| `dataObject` | `EntityType` `Concept` (one per object) | name = display name (matches OSI dataset name for ref consistency) | +| `dataObject` | `EntityType` `Concept` (one per object) | name = display name (matches Ossie dataset name for ref consistency) | | `dataObject.description` / `.comment` | `concept.description` | first non-empty wins | | `join` (A → B) | `Relationship` under A's component | declaring concept A is the implicit first role; B is an explicit `role` | | `join.joinType` | `Relationship.multiplicity` | `many-to-one`→`ManyToOne`, `one-to-one`→`OneToOne` | | `column.primaryKey` | entity `object_mappings[].expression` | `
.`; identifies the entity | | `join.columnsFrom` (FK) | `link_mappings[].object_mapping.expression` | `.`; binds the relationship to its far role | -| whole core model | `ontology_mappings[].semantic_model` | embedded verbatim from `OBMLtoOSI.convert()` | +| whole core model | `ontology_mappings[].semantic_model` | embedded verbatim from `OBMLtoOssie.convert()` | `
` is the final identifier of the dataset `source` (e.g. `db.schema.t` → `t`), falling back to the dataset name when `source` has no dotted physical table. @@ -77,9 +77,9 @@ falling back to the dataset name when `source` has no dotted physical table. | OBML construct | Handling | Reason | |----------------|----------|--------| -| `joinType: many-to-many` | relationship **skipped** + warning | OSI `Multiplicity` enum is only `ManyToOne`/`OneToOne` | +| `joinType: many-to-many` | relationship **skipped** + warning | Ossie `Multiplicity` enum is only `ManyToOne`/`OneToOne` | | missing/unknown `joinType` | defaults to `ManyToOne` + warning | matches core-converter default | -| secondary / `pathName` joins | emitted as ordinary relationships + warning | OSI ontology has no named-alternate-path concept | +| secondary / `pathName` joins | emitted as ordinary relationships + warning | Ossie ontology has no named-alternate-path concept | | composite primary/foreign keys | first column used + warning | `object_mapping.expression` is a single scalar SQL expression | | measures / metrics | **not** in the ontology layer | live only in the embedded core `semantic_model`; ontology models entities/relationships | | columns (non-key) as value concepts | not modeled (entities only) | keeps v1 valid and focused; ORM-style `ValueType` modeling deferred | @@ -87,15 +87,15 @@ falling back to the dataset name when `source` has no dotted physical table. ## Validation -`validate_osi_ontology()` runs semantic checks only (ossie ships no OSI ontology +`validate_ossie_ontology()` runs semantic checks only (ossie ships no Ossie ontology schema, and the converter emits ontology documents but never consumes them): unique concept names; relationship `roles` reference defined concepts; `concept_mappings` reference defined concepts. ## Stability note -OSI ontology is `0.2.0.dev0` (pre-1.0). This exporter is the supported, -schema-validated direction. An ontology **importer** (OSI ontology → OBML) is -intentionally deferred until OSI drops the `dev` pre-release suffix, because the -gaps above make the reverse direction lossy. Import the OSI **core spec** +Ossie ontology is `0.2.0.dev0` (pre-1.0). This exporter is the supported, +schema-validated direction. An ontology **importer** (Ossie ontology → OBML) is +intentionally deferred until Ossie drops the `dev` pre-release suffix, because the +gaps above make the reverse direction lossy. Import the Ossie **core spec** instead (already supported). diff --git a/converters/orionbelt/pyproject.toml b/converters/orionbelt/pyproject.toml index 041c4175..b0c456af 100644 --- a/converters/orionbelt/pyproject.toml +++ b/converters/orionbelt/pyproject.toml @@ -63,17 +63,17 @@ repository = "https://github.com/apache/ossie/" [tool.hatch.build.targets.wheel] packages = ["src/ossie_orionbelt"] -# The OSI core schema is not tracked as a copy in this package (it lives once at +# The Ossie core schema is not tracked as a copy in this package (it lives once at # the repo's core-spec/). Bundle it into the built wheel from that single source -# so a pip-installed converter still validates OSI documents self-contained. -# In-tree (dev/test) runs resolve it from core-spec/ via validation._osi_schema_path. +# so a pip-installed converter still validates Ossie documents self-contained. +# In-tree (dev/test) runs resolve it from core-spec/ via validation._ossie_schema_path. [tool.hatch.build.targets.wheel.force-include] -"../../core-spec/osi-schema.json" = "ossie_orionbelt/schemas/osi-schema.json" +"../../core-spec/ossie-schema.json" = "ossie_orionbelt/schemas/ossie-schema.json" # Only the OBML schema (OrionBelt's own format) is vendored under -# src/ossie_orionbelt/schemas/. OSI core documents are validated against the -# repo's core-spec/osi-schema.json, resolved at runtime by -# validation._osi_schema_path (no duplicate copy). The OSI ontology export is +# src/ossie_orionbelt/schemas/. Ossie core documents are validated against the +# repo's core-spec/ossie-schema.json, resolved at runtime by +# validation._ossie_schema_path (no duplicate copy). The Ossie ontology export is # validated semantically only (ossie ships no ontology schema). [tool.mypy] diff --git a/converters/orionbelt/src/ossie_orionbelt/__init__.py b/converters/orionbelt/src/ossie_orionbelt/__init__.py index 34715998..747aa1a5 100644 --- a/converters/orionbelt/src/ossie_orionbelt/__init__.py +++ b/converters/orionbelt/src/ossie_orionbelt/__init__.py @@ -15,44 +15,44 @@ # specific language governing permissions and limitations # under the License. -"""ossie-orionbelt: bidirectional OBML <-> OSI converter. +"""ossie-orionbelt: bidirectional OBML <-> Ossie converter. -Converts between OrionBelt Markup Language (OBML) semantic models and Open -Semantic Interchange (OSI) models, in both directions, plus an OSI ontology -emitter. Validation helpers check OBML and OSI documents against their JSON +Converts between OrionBelt Markup Language (OBML) semantic models and Ossie +models, in both directions, plus an Ossie ontology +emitter. Validation helpers check OBML and Ossie documents against their JSON schemas. Public API: - OSItoOBML - convert an OSI model dict to OBML - OBMLtoOSI - convert an OBML model dict to OSI core-spec - OBMLtoOSIOntology - emit an OSI ontology document from an OBML model + OssietoOBML - convert an Ossie model dict to OBML + OBMLtoOssie - convert an OBML model dict to Ossie core-spec + OBMLtoOssieOntology - emit an Ossie ontology document from an OBML model validate_obml - validate an OBML model dict - validate_osi - validate an OSI model dict - validate_osi_ontology - validate an OSI ontology document dict + validate_ossie - validate an Ossie model dict + validate_ossie_ontology - validate an Ossie ontology document dict ValidationResult - structured validation result """ from __future__ import annotations from ossie_orionbelt.converter import ( - OBMLtoOSI, - OBMLtoOSIOntology, - OSItoOBML, + OBMLtoOssie, + OBMLtoOssieOntology, + OssietoOBML, ValidationResult, validate_obml, - validate_osi, - validate_osi_ontology, + validate_ossie, + validate_ossie_ontology, ) __version__ = "0.1.0" __all__ = [ - "OBMLtoOSI", - "OBMLtoOSIOntology", - "OSItoOBML", + "OBMLtoOssie", + "OBMLtoOssieOntology", + "OssietoOBML", "ValidationResult", "validate_obml", - "validate_osi", - "validate_osi_ontology", + "validate_ossie", + "validate_ossie_ontology", "__version__", ] diff --git a/converters/orionbelt/src/ossie_orionbelt/_common.py b/converters/orionbelt/src/ossie_orionbelt/_common.py index f0e73a3c..9d4a5412 100644 --- a/converters/orionbelt/src/ossie_orionbelt/_common.py +++ b/converters/orionbelt/src/ossie_orionbelt/_common.py @@ -15,10 +15,10 @@ # specific language governing permissions and limitations # under the License. -"""Shared constants and mapping tables for the OSI ↔ OBML converter. +"""Shared constants and mapping tables for the Ossie ↔ OBML converter. These module-level constants are used by more than one of the converter -direction classes (``OSItoOBML``, ``OBMLtoOSI``, ``OBMLtoOSIOntology``) and the +direction classes (``OssietoOBML``, ``OBMLtoOssie``, ``OBMLtoOssieOntology``) and the validation helpers. They live here so both the facade ``converter`` module and the per-direction class modules can import them without forming an import cycle. """ @@ -28,12 +28,12 @@ import re # ─── Spec version pin ─────────────────────────────────────────────────────── -# Single source of truth for the OSI spec we emit. Bump when upstream cuts +# Single source of truth for the Ossie spec we emit. Bump when upstream cuts # a stable v0.2.0 (drop the ``.dev0`` suffix). All read paths accept both # 0.1.x (via the legacy shim) and 0.2.x. -_OSI_VERSION = "0.2.0.dev0" +_OSSIE_VERSION = "0.2.0.dev0" -# SQL dialects (of the OSI enum) whose aggregation expressions our regex-based +# SQL dialects (of the Ossie enum) whose aggregation expressions our regex-based # metric parser can read, in preference order. ANSI_SQL first; SNOWFLAKE and # DATABRICKS are SQL engines OrionBelt also targets, and their simple/expression # aggregations (``SUM(t.c)``, ``SUM(t.a * t.b)``) are syntactically identical to @@ -52,24 +52,24 @@ r'(?P[A-Za-z_]\w*|"[^"]+"|`[^`]+`|\[[^\]]+\])' ) # Vendor identities for custom_extensions. -# ORIONBELT - OrionBelt/OBML-proprietary payloads we author on OBML -> OSI. -# OSI - OSI-native fields OBML can't hold (unique_keys, field label, -# ai_context leftovers), stashed into OBML on OSI -> OBML. +# ORIONBELT - OrionBelt/OBML-proprietary payloads we author on OBML -> Ossie. +# Ossie - Ossie-native fields OBML can't hold (unique_keys, field label, +# ai_context leftovers), stashed into OBML on Ossie -> OBML. # Read paths also accept the legacy tags we emitted before this scheme so older # documents still round-trip; foreign vendors (SNOWFLAKE, DBT, ...) are # preserved verbatim, never relabelled. _VENDOR_OBML = "ORIONBELT" -_VENDOR_OSI = "OSI" +_VENDOR_OSSIE = "Ossie" _OBML_VENDOR_READ = ("ORIONBELT", "COMMON") -_OSI_VENDOR_READ = ("OSI", "OBSL") +_OSSIE_VENDOR_READ = ("Ossie", "OBSL") # Vendors the converter handles internally (its own payloads + native-field # stashes). Any custom_extension from a vendor outside this set is third-party # and is carried through verbatim in both directions, never relabelled. -_INTERNAL_VENDORS = frozenset({"ORIONBELT", "COMMON", "OSI", "OBSL"}) +_INTERNAL_VENDORS = frozenset({"ORIONBELT", "COMMON", "Ossie", "OBSL"}) # ─── Type mapping ─────────────────────────────────────────────────────────── -OBML_TO_OSI_TYPE = { +OBML_TO_OSSIE_TYPE = { "string": "string", "json": "string", "int": "integer", @@ -82,7 +82,7 @@ "boolean": "boolean", } -OSI_TO_OBML_TYPE = { +OSSIE_TO_OBML_TYPE = { "string": "string", "integer": "int", "number": "float", diff --git a/converters/orionbelt/src/ossie_orionbelt/cli.py b/converters/orionbelt/src/ossie_orionbelt/cli.py index ec70aef7..73283218 100644 --- a/converters/orionbelt/src/ossie_orionbelt/cli.py +++ b/converters/orionbelt/src/ossie_orionbelt/cli.py @@ -15,14 +15,14 @@ # specific language governing permissions and limitations # under the License. -"""Command-line entry point for the OBML <-> OSI converter. +"""Command-line entry point for the OBML <-> Ossie converter. A single ``ossie-orionbelt`` command with two format-named subcommands, mirroring -the OSI converter convention (e.g. ``osi-dbt msi-to-osi``): +the Ossie converter convention (e.g. ``ossie-dbt msi-to-ossie``): - ossie-orionbelt obml-to-osi -i model.obml.yaml -o model.osi.yaml - ossie-orionbelt obml-to-osi --ontology -i model.obml.yaml -o model.ontology.yaml - ossie-orionbelt osi-to-obml -i model.osi.yaml -o model.obml.yaml + ossie-orionbelt obml-to-ossie -i model.obml.yaml -o model.ossie.yaml + ossie-orionbelt obml-to-ossie --ontology -i model.obml.yaml -o model.ontology.yaml + ossie-orionbelt ossie-to-obml -i model.ossie.yaml -o model.obml.yaml Both subcommands print conversion warnings and a validation summary to stderr, and exit non-zero when the produced document fails schema validation (unless @@ -39,12 +39,12 @@ import yaml from ossie_orionbelt.converter import ( - OBMLtoOSI, - OBMLtoOSIOntology, - OSItoOBML, + OBMLtoOssie, + OBMLtoOssieOntology, + OssietoOBML, validate_obml, - validate_osi, - validate_osi_ontology, + validate_ossie, + validate_ossie_ontology, ) @@ -84,21 +84,21 @@ def _report_validation(label: str, result: dict[str, Any], validate_fn: Any) -> return True -def _cmd_obml_to_osi(args: argparse.Namespace) -> int: - """OBML -> OSI core-spec (or, with --ontology, OSI ontology).""" +def _cmd_obml_to_ossie(args: argparse.Namespace) -> int: + """OBML -> Ossie core-spec (or, with --ontology, Ossie ontology).""" data = _load(args.input) validate_fn: Any if args.ontology: - converter: Any = OBMLtoOSIOntology( + converter: Any = OBMLtoOssieOntology( data, args.model_name, args.description, args.ai_instructions ) result = converter.convert() - validate_fn, label = validate_osi_ontology, "OSI ontology output" + validate_fn, label = validate_ossie_ontology, "Ossie ontology output" else: - converter = OBMLtoOSI(data, args.model_name, args.description, args.ai_instructions) + converter = OBMLtoOssie(data, args.model_name, args.description, args.ai_instructions) result = converter.convert() - validate_fn, label = validate_osi, "OSI output" + validate_fn, label = validate_ossie, "Ossie output" _emit(result, args.output) _print_warnings(converter.warnings) @@ -108,11 +108,11 @@ def _cmd_obml_to_osi(args: argparse.Namespace) -> int: return 1 if _report_validation(label, result, validate_fn) else 0 -def _cmd_osi_to_obml(args: argparse.Namespace) -> int: - """OSI core-spec -> OBML.""" +def _cmd_ossie_to_obml(args: argparse.Namespace) -> int: + """Ossie core-spec -> OBML.""" data = _load(args.input) - converter = OSItoOBML(data, args.database, args.schema) + converter = OssietoOBML(data, args.database, args.schema) result = converter.convert() _emit(result, args.output) @@ -126,29 +126,29 @@ def _cmd_osi_to_obml(args: argparse.Namespace) -> int: def main(argv: list[str] | None = None) -> int: parser = argparse.ArgumentParser( prog="ossie-orionbelt", - description="Convert between OrionBelt OBML and OSI YAML.", + description="Convert between OrionBelt OBML and Ossie YAML.", ) subparsers = parser.add_subparsers(dest="command", required=True) - o2s = subparsers.add_parser("obml-to-osi", help="Convert OBML YAML → OSI YAML") + o2s = subparsers.add_parser("obml-to-ossie", help="Convert OBML YAML → Ossie YAML") o2s.add_argument("-i", "--input", required=True, metavar="FILE", help="Path to OBML YAML") o2s.add_argument( - "-o", "--output", required=True, metavar="FILE", help="Path for output OSI YAML" + "-o", "--output", required=True, metavar="FILE", help="Path for output Ossie YAML" ) o2s.add_argument( - "--ontology", action="store_true", help="Emit an OSI ontology document instead of core-spec" + "--ontology", action="store_true", help="Emit an Ossie ontology document instead of core-spec" ) o2s.add_argument( - "--model-name", default="semantic_model", metavar="NAME", help="OSI semantic model name" + "--model-name", default="semantic_model", metavar="NAME", help="Ossie semantic model name" ) - o2s.add_argument("--description", default="", metavar="TEXT", help="OSI model description") + o2s.add_argument("--description", default="", metavar="TEXT", help="Ossie model description") o2s.add_argument( - "--ai-instructions", default="", metavar="TEXT", help="OSI ai_context instructions" + "--ai-instructions", default="", metavar="TEXT", help="Ossie ai_context instructions" ) o2s.add_argument("--no-validate", action="store_true", help="Skip output validation") - s2o = subparsers.add_parser("osi-to-obml", help="Convert OSI YAML → OBML YAML") - s2o.add_argument("-i", "--input", required=True, metavar="FILE", help="Path to OSI YAML") + s2o = subparsers.add_parser("ossie-to-obml", help="Convert Ossie YAML → OBML YAML") + s2o.add_argument("-i", "--input", required=True, metavar="FILE", help="Path to Ossie YAML") s2o.add_argument( "-o", "--output", required=True, metavar="FILE", help="Path for output OBML YAML" ) @@ -161,10 +161,10 @@ def main(argv: list[str] | None = None) -> int: s2o.add_argument("--no-validate", action="store_true", help="Skip output validation") args = parser.parse_args(argv) - if args.command == "obml-to-osi": - return _cmd_obml_to_osi(args) - if args.command == "osi-to-obml": - return _cmd_osi_to_obml(args) + if args.command == "obml-to-ossie": + return _cmd_obml_to_ossie(args) + if args.command == "ossie-to-obml": + return _cmd_ossie_to_obml(args) return 1 diff --git a/converters/orionbelt/src/ossie_orionbelt/converter.py b/converters/orionbelt/src/ossie_orionbelt/converter.py index b5256173..8b5ed9c6 100644 --- a/converters/orionbelt/src/ossie_orionbelt/converter.py +++ b/converters/orionbelt/src/ossie_orionbelt/converter.py @@ -16,12 +16,12 @@ # specific language governing permissions and limitations # under the License. """ -OSI ↔ OBML Bidirectional Converter +Ossie ↔ OBML Bidirectional Converter =================================== Converts between Ossie (v0.2.0.dev0) YAML models and OrionBelt Markup Language (OBML v1.0) YAML models. -OSI v0.1.1 inputs are still accepted on read — the legacy shim +Ossie v0.1.1 inputs are still accepted on read — the legacy shim ``_normalize_legacy_v01`` promotes pre-v0.2 custom_extensions into the v0.2 first-class fields before regular parsing runs. @@ -31,9 +31,9 @@ sibling modules to keep each file focused: * :mod:`ossie_orionbelt._common` — shared constants and mapping tables -* :mod:`ossie_orionbelt.osi_to_obml` — :class:`OSItoOBML` -* :mod:`ossie_orionbelt.obml_to_osi` — :class:`OBMLtoOSI` -* :mod:`ossie_orionbelt.ontology` — :class:`OBMLtoOSIOntology` +* :mod:`ossie_orionbelt.ossie_to_obml` — :class:`OssietoOBML` +* :mod:`ossie_orionbelt.obml_to_ossie` — :class:`OBMLtoOssie` +* :mod:`ossie_orionbelt.ontology` — :class:`OBMLtoOssieOntology` * :mod:`ossie_orionbelt.validation` — :class:`ValidationResult` + ``validate_*`` Every public name is re-exported here so ``ossie_orionbelt.converter.`` @@ -58,10 +58,10 @@ _OBML_VENDOR_READ as _OBML_VENDOR_READ, ) from ossie_orionbelt._common import ( - _OSI_VENDOR_READ as _OSI_VENDOR_READ, + _OSSIE_VENDOR_READ as _OSSIE_VENDOR_READ, ) from ossie_orionbelt._common import ( - _OSI_VERSION as _OSI_VERSION, + _OSSIE_VERSION as _OSSIE_VERSION, ) from ossie_orionbelt._common import ( _SQL_PARSEABLE_DIALECTS as _SQL_PARSEABLE_DIALECTS, @@ -70,26 +70,26 @@ _VENDOR_OBML as _VENDOR_OBML, ) from ossie_orionbelt._common import ( - _VENDOR_OSI as _VENDOR_OSI, + _VENDOR_OSSIE as _VENDOR_OSSIE, ) # Shared constants / mapping tables (re-exported for backwards compatibility). # The ``X as X`` aliases mark these as intentional re-exports so historic # ``from ossie_orionbelt.converter import `` imports keep working. from ossie_orionbelt._common import ( - OBML_TO_OSI_TYPE as OBML_TO_OSI_TYPE, + OBML_TO_OSSIE_TYPE as OBML_TO_OSSIE_TYPE, ) from ossie_orionbelt._common import ( - OSI_TO_OBML_TYPE as OSI_TO_OBML_TYPE, + OSSIE_TO_OBML_TYPE as OSSIE_TO_OBML_TYPE, ) -from ossie_orionbelt.obml_to_osi import OBMLtoOSI as OBMLtoOSI -from ossie_orionbelt.ontology import OBMLtoOSIOntology as OBMLtoOSIOntology -from ossie_orionbelt.osi_to_obml import OSItoOBML as OSItoOBML +from ossie_orionbelt.obml_to_ossie import OBMLtoOssie as OBMLtoOssie +from ossie_orionbelt.ontology import OBMLtoOssieOntology as OBMLtoOssieOntology +from ossie_orionbelt.ossie_to_obml import OssietoOBML as OssietoOBML from ossie_orionbelt.validation import ( _OBML_SCHEMA_PATH as _OBML_SCHEMA_PATH, ) from ossie_orionbelt.validation import ( - _OSI_SCHEMA_PATH as _OSI_SCHEMA_PATH, + _OSSIE_SCHEMA_PATH as _OSSIE_SCHEMA_PATH, ) from ossie_orionbelt.validation import ( _SCHEMAS_DIR as _SCHEMAS_DIR, @@ -107,22 +107,22 @@ validate_obml as validate_obml, ) from ossie_orionbelt.validation import ( - validate_osi as validate_osi, + validate_ossie as validate_ossie, ) from ossie_orionbelt.validation import ( - validate_osi_ontology as validate_osi_ontology, + validate_ossie_ontology as validate_ossie_ontology, ) __all__ = [ - "OBML_TO_OSI_TYPE", - "OSI_TO_OBML_TYPE", - "OBMLtoOSI", - "OBMLtoOSIOntology", - "OSItoOBML", + "OBML_TO_OSSIE_TYPE", + "OSSIE_TO_OBML_TYPE", + "OBMLtoOssie", + "OBMLtoOssieOntology", + "OssietoOBML", "ValidationResult", "validate_obml", - "validate_osi", - "validate_osi_ontology", + "validate_ossie", + "validate_ossie_ontology", "main", ] @@ -133,23 +133,23 @@ def main(): - parser = argparse.ArgumentParser(description="OSI ↔ OBML Bidirectional Converter") - parser.add_argument("direction", choices=["osi2obml", "obml2osi"], help="Conversion direction") + parser = argparse.ArgumentParser(description="Ossie ↔ OBML Bidirectional Converter") + parser.add_argument("direction", choices=["ossie2obml", "obml2ossie"], help="Conversion direction") parser.add_argument("input", nargs="?", help="Input YAML file") parser.add_argument("-o", "--output", help="Output YAML file") parser.add_argument( - "--name", default="semantic_model", help="Model name for OBML→OSI conversion" + "--name", default="semantic_model", help="Model name for OBML→Ossie conversion" ) parser.add_argument( - "--description", default="", help="Model description for OBML→OSI conversion" + "--description", default="", help="Model description for OBML→Ossie conversion" ) parser.add_argument( - "--ai-instructions", default="", help="AI instructions for OBML→OSI conversion" + "--ai-instructions", default="", help="AI instructions for OBML→Ossie conversion" ) parser.add_argument( - "--database", default="ANALYTICS", help="Default database for OSI→OBML conversion" + "--database", default="ANALYTICS", help="Default database for Ossie→OBML conversion" ) - parser.add_argument("--schema", default="PUBLIC", help="Default schema for OSI→OBML conversion") + parser.add_argument("--schema", default="PUBLIC", help="Default schema for Ossie→OBML conversion") parser.add_argument( "--no-validate", action="store_true", help="Skip OBML validation after conversion" ) @@ -163,12 +163,12 @@ def main(): with open(input_path) as f: data = yaml.safe_load(f) - if args.direction == "osi2obml": - converter = OSItoOBML(data, args.database, args.schema) + if args.direction == "ossie2obml": + converter = OssietoOBML(data, args.database, args.schema) result = converter.convert() warnings = converter.warnings else: - converter = OBMLtoOSI(data, args.name, args.description, args.ai_instructions) + converter = OBMLtoOssie(data, args.name, args.description, args.ai_instructions) result = converter.convert() warnings = converter.warnings @@ -193,7 +193,7 @@ def main(): if not args.no_validate: has_errors = False - if args.direction == "osi2obml": + if args.direction == "ossie2obml": # Validate OBML output print("\n🔍 Validating OBML output...", file=sys.stderr) vr = validate_obml(result) @@ -205,7 +205,7 @@ def main(): print("❌ OBML output has validation errors", file=sys.stderr) has_errors = True else: - # Validate OBML input (source) and OSI output + # Validate OBML input (source) and Ossie output print("\n🔍 Validating OBML input...", file=sys.stderr) vr_obml = validate_obml(data) for line in vr_obml.summary_lines(): @@ -216,14 +216,14 @@ def main(): print("❌ OBML input has validation errors", file=sys.stderr) has_errors = True - print("\n🔍 Validating OSI output...", file=sys.stderr) - vr_osi = validate_osi(result) - for line in vr_osi.summary_lines(): + print("\n🔍 Validating Ossie output...", file=sys.stderr) + vr_ossie = validate_ossie(result) + for line in vr_ossie.summary_lines(): print(line, file=sys.stderr) - if vr_osi.valid: - print("✅ OSI output is valid", file=sys.stderr) + if vr_ossie.valid: + print("✅ Ossie output is valid", file=sys.stderr) else: - print("❌ OSI output has validation errors", file=sys.stderr) + print("❌ Ossie output has validation errors", file=sys.stderr) has_errors = True if has_errors: diff --git a/converters/orionbelt/src/ossie_orionbelt/obml_to_osi.py b/converters/orionbelt/src/ossie_orionbelt/obml_to_ossie.py similarity index 87% rename from converters/orionbelt/src/ossie_orionbelt/obml_to_osi.py rename to converters/orionbelt/src/ossie_orionbelt/obml_to_ossie.py index db9ce97b..d46687eb 100644 --- a/converters/orionbelt/src/ossie_orionbelt/obml_to_osi.py +++ b/converters/orionbelt/src/ossie_orionbelt/obml_to_ossie.py @@ -15,7 +15,7 @@ # specific language governing permissions and limitations # under the License. -"""OBML → OSI conversion (the :class:`OBMLtoOSI` direction). +"""OBML → Ossie conversion (the :class:`OBMLtoOssie` direction). Extracted verbatim from ``converter.py``; see that module for the package-level docstring and the shared constants in :mod:`ossie_orionbelt._common`. @@ -29,15 +29,15 @@ from ossie_orionbelt._common import ( _INTERNAL_VENDORS, - _OSI_VENDOR_READ, - _OSI_VERSION, + _OSSIE_VENDOR_READ, + _OSSIE_VERSION, _VENDOR_OBML, - OBML_TO_OSI_TYPE, + OBML_TO_OSSIE_TYPE, ) -class OBMLtoOSI: - """Convert an OBML semantic model YAML to OSI format.""" +class OBMLtoOssie: + """Convert an OBML semantic model YAML to Ossie format.""" def __init__( self, @@ -57,7 +57,7 @@ def convert(self) -> dict: # not duplicate them. self.warnings = [] - osi: dict[str, Any] = {"version": _OSI_VERSION} + ossie: dict[str, Any] = {"version": _OSSIE_VERSION} data_objects = self.obml.get("dataObjects", {}) obml_dimensions = self.obml.get("dimensions", {}) @@ -73,14 +73,14 @@ def convert(self) -> dict: datasets.append(dataset) all_relationships.extend(rels) - # ── Metrics (OBML measures + metrics → OSI metrics) ──────── - osi_metrics = self._convert_measures_and_metrics(obml_measures, obml_metrics, data_objects) + # ── Metrics (OBML measures + metrics → Ossie metrics) ──────── + ossie_metrics = self._convert_measures_and_metrics(obml_measures, obml_metrics, data_objects) - # Re-emit OSI metrics that OBML could not represent and that the import - # path preserved verbatim (vendor OSI, ``obml_unconverted_metrics``). - # This closes the OSI -> OBML -> OSI roundtrip for non-SQL or + # Re-emit Ossie metrics that OBML could not represent and that the import + # path preserved verbatim (vendor Ossie, ``obml_unconverted_metrics``). + # This closes the Ossie -> OBML -> Ossie roundtrip for non-SQL or # non-decomposable metrics. - self._merge_restored_metrics(osi_metrics) + self._merge_restored_metrics(ossie_metrics) # ── Build semantic model ──────────────────────────────────── sem_model: dict[str, Any] = {"name": self.model_name} @@ -97,8 +97,8 @@ def convert(self) -> dict: if all_relationships: sem_model["relationships"] = all_relationships - if osi_metrics: - sem_model["metrics"] = osi_metrics + if ossie_metrics: + sem_model["metrics"] = ossie_metrics # Add OBML as custom extension for lossless roundtrip info roundtrip_data: dict[str, Any] = { @@ -118,7 +118,7 @@ def convert(self) -> dict: obml_owner = self.obml.get("owner") if obml_owner: roundtrip_data["obml_owner"] = obml_owner - # Preserve count-synthesis knobs (OSI has no native equivalent). The + # Preserve count-synthesis knobs (Ossie has no native equivalent). The # synthesized ``.count`` measures themselves are NOT emitted — # they are derived and regenerate on load — but the knobs must survive # a roundtrip. ``is not None`` so an explicit ``exposeCounts: false`` is @@ -140,44 +140,44 @@ def convert(self) -> dict: self.obml.get("customExtensions"), sem_model["custom_extensions"] ) - osi["semantic_model"] = [sem_model] + ossie["semantic_model"] = [sem_model] - # The published OSI core schema forbids root-level ``dialects`` / + # The published Ossie core schema forbids root-level ``dialects`` / # ``vendors`` (root is additionalProperties:false, only ``version`` + # ``semantic_model``). Dialects live per-expression in # ``expression.dialects[]`` and vendors per-entity in # ``custom_extensions[].vendor_name`` — the schema-valid homes — so the # document stays fully conformant without root advertisement arrays. - # See OSI PR #148 (and the single-document-dialect direction in #52). - return osi + # See Ossie PR #148 (and the single-document-dialect direction in #52). + return ossie - def _emit_foreign_extensions(self, obml_exts: list[dict] | None, osi_exts: list[dict]) -> None: - """Re-emit third-party OBML customExtensions as OSI custom_extensions. + def _emit_foreign_extensions(self, obml_exts: list[dict] | None, ossie_exts: list[dict]) -> None: + """Re-emit third-party OBML customExtensions as Ossie custom_extensions. - Mirrors ``OSItoOBML._carry_foreign_extensions``: extensions from a - vendor we do not handle internally are passed back to OSI under their + Mirrors ``OssietoOBML._carry_foreign_extensions``: extensions from a + vendor we do not handle internally are passed back to Ossie under their original vendor name, completing the roundtrip. """ for ext in obml_exts or []: vendor = ext.get("vendor") if vendor and vendor not in _INTERNAL_VENDORS: - osi_exts.append({"vendor_name": vendor, "data": ext.get("data", "")}) + ossie_exts.append({"vendor_name": vendor, "data": ext.get("data", "")}) def _convert_data_object( self, do_name: str, do_obj: dict, obml_dimensions: dict ) -> tuple[dict, list]: - """Convert an OBML dataObject to an OSI dataset + relationships.""" + """Convert an OBML dataObject to an Ossie dataset + relationships.""" database = do_obj.get("database", "") schema = do_obj.get("schema", "") code = do_obj.get("code", "") source = f"{database}.{schema}.{code}" if database else code - # Use the OBML display name as the OSI dataset name so that + # Use the OBML display name as the Ossie dataset name so that # relationship references (joinTo) stay consistent in roundtrips - osi_name = do_name + ossie_name = do_name dataset: dict[str, Any] = { - "name": osi_name, + "name": ossie_name, "source": source, } @@ -190,7 +190,7 @@ def _convert_data_object( for col_name, col in (do_obj.get("columns", {}) or {}).items() if col.get("primaryKey") ] - # Use the physical ``code`` for each column when present — OSI + # Use the physical ``code`` for each column when present — Ossie # field names mirror the physical column code (see _convert_column). if pk_columns: columns_map = do_obj.get("columns", {}) or {} @@ -201,10 +201,10 @@ def _convert_data_object( # ── Unique keys (v0.2 first-class, lossless roundtrip via OBSL) ── # OBML doesn't model unique keys natively today; round-trip via the # ``OBSL``-vendor ``obml_unique_keys`` payload that originated from - # a prior OSI → OBML conversion (or hand-authored OBML). + # a prior Ossie → OBML conversion (or hand-authored OBML). unique_keys_extra: list[list[str]] | None = None for ext in do_obj.get("customExtensions", []) or []: - if ext.get("vendor") not in _OSI_VENDOR_READ: + if ext.get("vendor") not in _OSSIE_VENDOR_READ: continue try: data = json.loads(ext.get("data", "{}")) @@ -225,7 +225,7 @@ def _convert_data_object( # ── Rebuild ai_context: native synonyms + remaining from customExtensions ai_ctx: dict[str, Any] = {} for ext in do_obj.get("customExtensions", []): - if ext.get("vendor") == "OSI": + if ext.get("vendor") == "Ossie": try: ai_data = json.loads(ext.get("data", "{}")) if ai_data: @@ -255,7 +255,7 @@ def _convert_data_object( relationships = [] joins = do_obj.get("joins", []) for i, join in enumerate(joins): - rel = self._convert_join_to_relationship(osi_name, do_name, do_obj, join, i) + rel = self._convert_join_to_relationship(ossie_name, do_name, do_obj, join, i) if rel: relationships.append(rel) @@ -265,8 +265,8 @@ def _convert_data_object( do_extras["obml_owner"] = do_obj["owner"] if do_obj.get("comment"): do_extras["obml_comment"] = do_obj["comment"] - # OBML-only freshness contract — round-tripped through OSI - # custom_extensions since OSI has no native equivalent. See + # OBML-only freshness contract — round-tripped through Ossie + # custom_extensions since Ossie has no native equivalent. See # design/PLAN_freshness_driven_cache.md §5. if do_obj.get("refresh"): do_extras["obml_refresh"] = do_obj["refresh"] @@ -296,7 +296,7 @@ def _convert_data_object( def _convert_column( self, col_name: str, col_obj: dict, do_name: str, obml_dimensions: dict ) -> dict: - """Convert an OBML column to an OSI field.""" + """Convert an OBML column to an Ossie field.""" code = col_obj.get("code", col_name.lower().replace(" ", "_")) field: dict[str, Any] = { @@ -340,12 +340,12 @@ def _convert_column( else: field["description"] = col_name # Use display name as description - # ── Field label (OSI v0.2 first-class) ── + # ── Field label (Ossie v0.2 first-class) ── # Surfaced from OBSL-vendor customExtensions ``obml_field_label`` — - # round-trip path for OSI → OBML → OSI fidelity. OBML has no + # round-trip path for Ossie → OBML → Ossie fidelity. OBML has no # native column ``label`` today. for ext in col_obj.get("customExtensions", []) or []: - if ext.get("vendor") not in _OSI_VENDOR_READ: + if ext.get("vendor") not in _OSSIE_VENDOR_READ: continue try: ext_label_data = json.loads(ext.get("data", "{}")) @@ -355,10 +355,10 @@ def _convert_column( field["label"] = ext_label_data["obml_field_label"] break - # Restore ai_context from customExtensions (OSI vendor) if present + # Restore ai_context from customExtensions (Ossie vendor) if present ai_ctx: dict[str, Any] = {} for ext in col_obj.get("customExtensions", []): - if ext.get("vendor") == "OSI": + if ext.get("vendor") == "Ossie": try: ai_data = json.loads(ext.get("data", "{}")) if ai_data: @@ -387,9 +387,9 @@ def _convert_column( # Preserve OBML type info in custom_extensions for roundtrip fidelity abstract_type = col_obj.get("abstractType", "string") - osi_type = OBML_TO_OSI_TYPE.get(abstract_type, "string") + ossie_type = OBML_TO_OSSIE_TYPE.get(abstract_type, "string") ext_data: dict[str, Any] = { - "data_type": osi_type, + "data_type": ossie_type, "obml_abstract_type": abstract_type, } # Preserve OBML-only column properties @@ -430,7 +430,7 @@ def _convert_column( } ] - # Re-emit third-party vendor extensions verbatim. OSI has no separate + # Re-emit third-party vendor extensions verbatim. Ossie has no separate # dimension entity, so a matched dimension's foreign extensions surface # on the field too (they re-import onto the column). self._emit_foreign_extensions(col_obj.get("customExtensions"), field["custom_extensions"]) @@ -442,12 +442,12 @@ def _convert_column( return field def _convert_join_to_relationship( - self, osi_from_name: str, _obml_from_name: str, from_do: dict, join: dict, index: int + self, ossie_from_name: str, _obml_from_name: str, from_do: dict, join: dict, index: int ) -> dict | None: - """Convert an OBML join to an OSI relationship.""" + """Convert an OBML join to an Ossie relationship.""" join_to_display = join.get("joinTo", "") - # Use the OBML display name as the OSI target name (consistent with - # _convert_data_object which uses display name as OSI dataset name) + # Use the OBML display name as the Ossie target name (consistent with + # _convert_data_object which uses display name as Ossie dataset name) to_name = join_to_display target_do = self.obml.get("dataObjects", {}).get(join_to_display, {}) @@ -461,15 +461,15 @@ def _convert_join_to_relationship( # Generate relationship name path_name = join.get("pathName", "") if path_name: - rel_name = f"{osi_from_name}_to_{to_name}_{path_name}" + rel_name = f"{ossie_from_name}_to_{to_name}_{path_name}" else: - rel_name = f"{osi_from_name}_to_{to_name}" + rel_name = f"{ossie_from_name}_to_{to_name}" if index > 0: rel_name += f"_{index}" rel: dict[str, Any] = { "name": rel_name, - "from": osi_from_name, + "from": ossie_from_name, "to": to_name, "from_columns": from_cols, "to_columns": to_cols, @@ -497,15 +497,15 @@ def _resolve_column_codes(self, do_obj: dict, col_display_names: list) -> list: return codes def _restore_unconverted_metrics(self) -> list[dict]: - """Recover OSI metrics preserved verbatim during OSI -> OBML import. + """Recover Ossie metrics preserved verbatim during Ossie -> OBML import. - The import path stashes metrics OBML can't represent under an OSI-vendor + The import path stashes metrics OBML can't represent under an Ossie-vendor model-level customExtension (``obml_unconverted_metrics``). Re-emit them - unchanged so a full OSI -> OBML -> OSI roundtrip keeps them. + unchanged so a full Ossie -> OBML -> Ossie roundtrip keeps them. """ restored: list[dict] = [] for ext in self.obml.get("customExtensions", []) or []: - if ext.get("vendor") not in _OSI_VENDOR_READ: + if ext.get("vendor") not in _OSSIE_VENDOR_READ: continue try: data = json.loads(ext.get("data", "{}")) @@ -516,76 +516,76 @@ def _restore_unconverted_metrics(self) -> list[dict]: restored.extend(m for m in preserved if isinstance(m, dict)) return restored - def _merge_restored_metrics(self, osi_metrics: list[dict]) -> None: - """Append preserved (unconverted) OSI metrics to the converted ones. + def _merge_restored_metrics(self, ossie_metrics: list[dict]) -> None: + """Append preserved (unconverted) Ossie metrics to the converted ones. Name-collision guard: if a queryable OBML measure/metric now owns a name a stale preserved metric also uses, skip the preserved copy (the real - metric wins) so the OSI output has no duplicate metric names and passes + metric wins) so the Ossie output has no duplicate metric names and passes semantic validation. Each appended metric carries its own dialects (``expression.dialects[]``) and vendors (``custom_extensions``) verbatim, which are the schema-valid homes for that metadata. """ - existing = {m.get("name") for m in osi_metrics if isinstance(m, dict)} + existing = {m.get("name") for m in ossie_metrics if isinstance(m, dict)} for restored in self._restore_unconverted_metrics(): name = restored.get("name") if name in existing: self.warnings.append( - f"Preserved OSI metric '{name}' dropped on export: a converted " + f"Preserved Ossie metric '{name}' dropped on export: a converted " f"OBML metric now uses that name." ) continue existing.add(name) - osi_metrics.append(restored) + ossie_metrics.append(restored) def _convert_measures_and_metrics( self, obml_measures: dict, obml_metrics: dict, data_objects: dict ) -> list: - """Convert OBML measures and metrics to OSI metrics.""" - osi_metrics = [] + """Convert OBML measures and metrics to Ossie metrics.""" + ossie_metrics = [] - # Convert each OBML measure to an OSI metric + # Convert each OBML measure to an Ossie metric for measure_name, measure_obj in obml_measures.items(): - osi_metric = self._convert_measure(measure_name, measure_obj, data_objects) - if osi_metric: - self._carry_foreign_to_osi_metric(measure_obj, osi_metric) - osi_metrics.append(osi_metric) + ossie_metric = self._convert_measure(measure_name, measure_obj, data_objects) + if ossie_metric: + self._carry_foreign_to_ossie_metric(measure_obj, ossie_metric) + ossie_metrics.append(ossie_metric) - # Convert OBML metrics (which reference measures) to OSI metrics + # Convert OBML metrics (which reference measures) to Ossie metrics for metric_name, metric_obj in obml_metrics.items(): if metric_obj.get("type") == "cumulative": - osi_metric = self._convert_obml_cumulative_metric( + ossie_metric = self._convert_obml_cumulative_metric( metric_name, metric_obj, obml_measures, data_objects ) elif metric_obj.get("type") == "period_over_period": - osi_metric = self._convert_obml_pop_metric( + ossie_metric = self._convert_obml_pop_metric( metric_name, metric_obj, obml_measures, data_objects ) elif metric_obj.get("type") == "window": - osi_metric = self._convert_obml_window_metric( + ossie_metric = self._convert_obml_window_metric( metric_name, metric_obj, obml_measures, data_objects ) else: - osi_metric = self._convert_obml_metric( + ossie_metric = self._convert_obml_metric( metric_name, metric_obj, obml_measures, data_objects ) - if osi_metric: - self._carry_foreign_to_osi_metric(metric_obj, osi_metric) - osi_metrics.append(osi_metric) + if ossie_metric: + self._carry_foreign_to_ossie_metric(metric_obj, ossie_metric) + ossie_metrics.append(ossie_metric) - return osi_metrics + return ossie_metrics - def _carry_foreign_to_osi_metric(self, obml_obj: dict, osi_metric: dict) -> None: + def _carry_foreign_to_ossie_metric(self, obml_obj: dict, ossie_metric: dict) -> None: """Re-emit third-party vendor extensions on an OBML measure/metric to - the OSI metric, dropping the key again if nothing foreign was added.""" + the Ossie metric, dropping the key again if nothing foreign was added.""" self._emit_foreign_extensions( - obml_obj.get("customExtensions"), osi_metric.setdefault("custom_extensions", []) + obml_obj.get("customExtensions"), ossie_metric.setdefault("custom_extensions", []) ) - if not osi_metric["custom_extensions"]: - del osi_metric["custom_extensions"] + if not ossie_metric["custom_extensions"]: + del ossie_metric["custom_extensions"] def _convert_measure(self, name: str, measure: dict, data_objects: dict) -> dict | None: - """Convert an OBML measure to an OSI metric.""" + """Convert an OBML measure to an Ossie metric.""" columns = measure.get("columns", []) agg = measure.get("aggregation", "sum").upper() @@ -598,9 +598,9 @@ def _convert_measure(self, name: str, measure: dict, data_objects: dict) -> dict # ``aggregation: measure`` delegates resolution to the engine # (Databricks Metric View) — there is no source column to read - # and no ANSI_SQL expression to emit. OSI has no first-class + # and no ANSI_SQL expression to emit. Ossie has no first-class # concept for engine-delegated aggregation, so we serialize the - # measure as an OSI metric whose expression is the literal + # measure as an Ossie metric whose expression is the literal # ``MEASURE(" *
CalculatedN/AMODEL.semanticCalculatedDimensions
* - * @param osiDataset The Ossie dataset + * @param ossieDataset The Ossie dataset * @param sfDataObject The Salesforce data object to add direct fields to * @param outputData The Salesforce model for adding calculated dimensions */ private void processFieldsForDataset( - Map osiDataset, Map sfDataObject, Map outputData) { - List osiFields = getList(osiDataset, FIELDS); - if (osiFields == null) { + Map ossieDataset, Map sfDataObject, Map outputData) { + List ossieFields = getList(ossieDataset, FIELDS); + if (ossieFields == null) { return; } List sfDimensions = getList(sfDataObject, SEMANTIC_DIMENSIONS); List sfMeasurements = getList(sfDataObject, SEMANTIC_MEASUREMENTS); - for (Object osiFieldObj : osiFields) { - Map osiField = asMap(osiFieldObj); + for (Object ossieFieldObj : ossieFields) { + Map ossieField = asMap(ossieFieldObj); // Determine field type based on Ossie structure - boolean hasDimension = osiField.containsKey(DIMENSION); - ExpressionInfo expressionInfo = unwrapExpression(osiField); + boolean hasDimension = ossieField.containsKey(DIMENSION); + ExpressionInfo expressionInfo = unwrapExpression(ossieField); String expression = expressionInfo.expression(); String dialect = expressionInfo.dialect(); @@ -308,11 +308,11 @@ private void processFieldsForDataset( if (isCalculated) { // Create a semantic calculated dimension - Map calcDim = createSemanticCalculatedDimension(osiField, expression); + Map calcDim = createSemanticCalculatedDimension(ossieField, expression); - customExtensionHandler.restoreSalesforceCustomExtension(calcDim, osiField); + customExtensionHandler.restoreSalesforceCustomExtension(calcDim, ossieField); - applyOsiDatatype(calcDim, osiField); + applyOssieDatatype(calcDim, ossieField); applyFieldDefaults(calcDim); @@ -323,11 +323,11 @@ private void processFieldsForDataset( // Non calculated field - add to data object FieldType fieldType = hasDimension? FieldType.DIMENSION : FieldType.MEASUREMENT; - Map sfField = mapFieldProperties(osiField, expression); + Map sfField = mapFieldProperties(ossieField, expression); - customExtensionHandler.restoreSalesforceCustomExtension(sfField, osiField); + customExtensionHandler.restoreSalesforceCustomExtension(sfField, ossieField); - applyOsiDatatype(sfField, osiField); + applyOssieDatatype(sfField, ossieField); applyFieldDefaults(sfField); @@ -344,24 +344,24 @@ private void processFieldsForDataset( * Maps field properties based on whether the field is calculated. * Includes common properties plus type-specific properties. * - * @param osiField The Ossie field + * @param ossieField The Ossie field * @param expression The extracted expression string * @return A map with Salesforce field properties */ private Map mapFieldProperties( - Map osiField, String expression) { + Map ossieField, String expression) { Map sfField = new LinkedHashMap<>(); - String name = getString(osiField, NAME); + String name = getString(ossieField, NAME); sfField.put(API_NAME, name); - String description = getString(osiField, DESCRIPTION); + String description = getString(ossieField, DESCRIPTION); if (description != null) { sfField.put(DESCRIPTION, description); } - String label = getString(osiField, LABEL); + String label = getString(ossieField, LABEL); if (label != null) { sfField.put(LABEL, label); } @@ -374,27 +374,27 @@ private Map mapFieldProperties( * Creates a Salesforce semanticCalculatedDimension from an Ossie field with a calculated expression. * Per schema: required properties are apiName and expression. * - * @param osiField The Ossie field + * @param ossieField The Ossie field * @param expression The calculated expression * @return A map representing a semanticCalculatedDimension */ private Map createSemanticCalculatedDimension( - Map osiField, String expression) { + Map ossieField, String expression) { Map calcDim = new LinkedHashMap<>(); // Required properties - String name = getString(osiField, NAME); + String name = getString(ossieField, NAME); calcDim.put(API_NAME, name); calcDim.put(EXPRESSION, expression); // Optional properties - String description = getString(osiField, DESCRIPTION); + String description = getString(ossieField, DESCRIPTION); if (description != null) { calcDim.put(DESCRIPTION, description); } - String label = getString(osiField, LABEL); + String label = getString(ossieField, LABEL); if (label != null) { calcDim.put(LABEL, label); } @@ -453,11 +453,11 @@ private record ExpressionInfo(String expression, String dialect) {} * Extracts the expression value and dialect from Ossie field's expression.dialects[0].expression. * This unwraps the nested structure to get the simple column reference and its dialect. * - * @param osiField The Ossie field containing expression structure + * @param ossieField The Ossie field containing expression structure * @return ExpressionInfo containing the expression string and dialect type, or null if not found */ - private ExpressionInfo unwrapExpression(Map osiField) { - Object expressionObj = osiField.get(EXPRESSION); + private ExpressionInfo unwrapExpression(Map ossieField) { + Object expressionObj = ossieField.get(EXPRESSION); Map expression = asMap(expressionObj); Object dialectsObj = expression.get(DIALECTS); @@ -553,44 +553,44 @@ private void applyFieldDefaults(Map sfField) { * restored from custom_extensions. Exact extension data wins to preserve * Salesforce-specific distinctions such as Email and Currency. */ - private void applyOsiDatatype(Map sfField, Map osiField) { - String osiDatatype = getString(osiField, OSI_DATATYPE); + private void applyOssieDatatype(Map sfField, Map ossieField) { + String ossieDatatype = getString(ossieField, OSSIE_DATATYPE); String exactSalesforceDataType = getString(sfField, DATA_TYPE); - String mappedSalesforceDataType = SalesforceDataTypeMapper.toSalesforce(osiDatatype); + String mappedSalesforceDataType = SalesforceDataTypeMapper.toSalesforce(ossieDatatype); String selectedSalesforceDataType = exactSalesforceDataType != null ? exactSalesforceDataType : mappedSalesforceDataType; if (SalesforceDataTypeMapper.isTimezoneLossyMapping( - osiDatatype, selectedSalesforceDataType)) { + ossieDatatype, selectedSalesforceDataType)) { logger.warn( "Field '{}' has Ossie datatype 'DateTime'; Salesforce dataType 'DateTime' " + "cannot preserve the timezone-free distinction and re-imports as 'DateTimeTz'", - getString(osiField, NAME)); + getString(ossieField, NAME)); } if (exactSalesforceDataType != null) { - if (osiDatatype != null - && !SalesforceDataTypeMapper.areCompatible(osiDatatype, exactSalesforceDataType)) { + if (ossieDatatype != null + && !SalesforceDataTypeMapper.areCompatible(ossieDatatype, exactSalesforceDataType)) { logger.warn( "Field '{}' has Ossie datatype '{}' that conflicts with exact Salesforce dataType '{}'; " + "preserving the Salesforce extension value", - getString(osiField, NAME), - osiDatatype, + getString(ossieField, NAME), + ossieDatatype, exactSalesforceDataType); } return; } - if (osiDatatype == null) { + if (ossieDatatype == null) { return; } if (mappedSalesforceDataType == null) { logger.warn( "Field '{}' has Ossie datatype '{}' with no safe Salesforce mapping; omitting dataType", - getString(osiField, NAME), - osiDatatype); + getString(ossieField, NAME), + ossieDatatype); return; } sfField.put(DATA_TYPE, mappedSalesforceDataType); @@ -620,7 +620,7 @@ private void processModelLevelCalculatedDimensions( logger.debug("Processing {} semanticCalculatedDimensions", calcDims.size()); - List osiDatasets = getList(outputData, DATASETS); + List ossieDatasets = getList(outputData, DATASETS); List remainingCalcDims = new ArrayList<>(); for (Object calcDimObj : calcDims) { @@ -630,11 +630,11 @@ private void processModelLevelCalculatedDimensions( String targetDataObject = getSingleDataObjectFromDependencies(dependencies); if (targetDataObject != null) { - Map osiDataset = findItemById(osiDatasets, NAME, targetDataObject); - if (osiDataset != null) { - Map osiField = convertCalculatedDimensionToField(calcDim); - List fields = getOrCreateList(osiDataset, FIELDS); - fields.add(osiField); + Map ossieDataset = findItemById(ossieDatasets, NAME, targetDataObject); + if (ossieDataset != null) { + Map ossieField = convertCalculatedDimensionToField(calcDim); + List fields = getOrCreateList(ossieDataset, FIELDS); + fields.add(ossieField); // Update relationships for this converted field String calcFieldName = getString(calcDim, API_NAME); @@ -692,16 +692,16 @@ private String getSingleDataObjectFromDependencies(List dependencies) { /** * Converts a Salesforce semanticCalculatedDimension to an Ossie field with dimension property. - * Similar to convertDimensionToOsiField but handles expression (not dataObjectFieldName). + * Similar to convertDimensionToOssieField but handles expression (not dataObjectFieldName). * * @param calcDim The Salesforce calculated dimension * @return An Ossie field map with dimension property and wrapped expression */ private Map convertCalculatedDimensionToField(Map calcDim) { - Map osiField = new LinkedHashMap<>(); + Map ossieField = new LinkedHashMap<>(); // Common properties: name, label, description - mapCommonFieldProperties(calcDim, osiField); + mapCommonFieldProperties(calcDim, ossieField); // Add dimension property with is_time based on dataType Map dimensionProp = new LinkedHashMap<>(); @@ -711,17 +711,17 @@ private Map convertCalculatedDimensionToField(Map handledProps = Set.of(API_NAME, LABEL, DESCRIPTION, EXPRESSION, DEPENDENCIES); - customExtensionHandler.storeUnmappedItemProperties(osiField, calcDim, handledProps); - return osiField; + customExtensionHandler.storeUnmappedItemProperties(ossieField, calcDim, handledProps); + return ossieField; } /** diff --git a/converters/salesforce/src/main/java/org/apache/ossie/converter/MetricMappingHandler.java b/converters/salesforce/src/main/java/org/apache/ossie/converter/MetricMappingHandler.java index a84e3f40..6add2b9b 100644 --- a/converters/salesforce/src/main/java/org/apache/ossie/converter/MetricMappingHandler.java +++ b/converters/salesforce/src/main/java/org/apache/ossie/converter/MetricMappingHandler.java @@ -58,21 +58,21 @@ public MetricMappingHandler(ConversionDirection direction, CustomExtensionHandle @Override public void execute(Map sourceData, Map outputData, Map mappings) { logger.debug("Mapping metrics in {} direction", direction); - if (direction == ConversionDirection.OSI_TO_SALESFORCE) { - mapOsiToSalesforce(sourceData, outputData, mappings); + if (direction == ConversionDirection.OSSIE_TO_SALESFORCE) { + mapOssieToSalesforce(sourceData, outputData, mappings); } else { - mapSalesforceToOsi(sourceData, outputData, mappings); + mapSalesforceToOssie(sourceData, outputData, mappings); } } /** * Maps Ossie metrics to Salesforce semanticCalculatedMeasurements. */ - private void mapOsiToSalesforce( + private void mapOssieToSalesforce( Map sourceData, Map outputData, Map mappings) { - List osiMetrics = getList(sourceData, METRICS); - if (osiMetrics == null) { + List ossieMetrics = getList(sourceData, METRICS); + if (ossieMetrics == null) { return; } @@ -86,7 +86,7 @@ private void mapOsiToSalesforce( /** * Maps Salesforce semanticCalculatedMeasurements to Ossie metrics. */ - private void mapSalesforceToOsi( + private void mapSalesforceToOssie( Map sourceData, Map outputData, Map mappings) { List sfMetrics = getList(sourceData, SEMANTIC_CALCULATED_MEASUREMENTS); @@ -105,9 +105,9 @@ private void mapSalesforceToOsi( outputData.putAll(mappedData); - List osiMetrics = getList(outputData, METRICS); - if (osiMetrics != null) { - wrapExpressions(sfMetrics, osiMetrics); + List ossieMetrics = getList(outputData, METRICS); + if (ossieMetrics != null) { + wrapExpressions(sfMetrics, ossieMetrics); } // Store unmapped SF properties in custom_extensions @@ -121,21 +121,21 @@ private void mapSalesforceToOsi( /** * Wraps expressions for SF→Ossie conversion. */ - private void wrapExpressions(List sfMetrics, List osiMetrics) { - for (int i = 0; i < sfMetrics.size() && i < osiMetrics.size(); i++) { + private void wrapExpressions(List sfMetrics, List ossieMetrics) { + for (int i = 0; i < sfMetrics.size() && i < ossieMetrics.size(); i++) { Map sfMetric = asMap(sfMetrics.get(i)); - Map osiMetric = asMap(osiMetrics.get(i)); + Map ossieMetric = asMap(ossieMetrics.get(i)); // Get expression from SF metric String expressionValue = getString(sfMetric, EXPRESSION); if (expressionValue != null) { // Wrap in Ossie dialect structure - osiMetric.put(EXPRESSION, wrapExpression(expressionValue)); + ossieMetric.put(EXPRESSION, wrapExpression(expressionValue)); } String datatype = SalesforceDataTypeMapper.toOssie(getString(sfMetric, DATA_TYPE)); if (datatype != null) { - osiMetric.put(OSI_DATATYPE, datatype); + ossieMetric.put(OSSIE_DATATYPE, datatype); } } } diff --git a/converters/salesforce/src/main/java/org/apache/ossie/converter/RelationshipMappingHandler.java b/converters/salesforce/src/main/java/org/apache/ossie/converter/RelationshipMappingHandler.java index 92f67d0c..20c4949b 100644 --- a/converters/salesforce/src/main/java/org/apache/ossie/converter/RelationshipMappingHandler.java +++ b/converters/salesforce/src/main/java/org/apache/ossie/converter/RelationshipMappingHandler.java @@ -55,26 +55,26 @@ public RelationshipMappingHandler(ConversionDirection direction, CustomExtension @Override public void execute(Map sourceData, Map outputData, Map mappings) { logger.debug("Mapping relationships in {} direction", direction); - if (direction == ConversionDirection.OSI_TO_SALESFORCE) { - mapOsiToSalesforce(sourceData, outputData, mappings); + if (direction == ConversionDirection.OSSIE_TO_SALESFORCE) { + mapOssieToSalesforce(sourceData, outputData, mappings); } else { - mapSalesforceToOsi(sourceData, outputData, mappings); + mapSalesforceToOssie(sourceData, outputData, mappings); } } /** * Maps Ossie relationships to Salesforce semanticRelationships. */ - private void mapOsiToSalesforce( + private void mapOssieToSalesforce( Map sourceData, Map outputData, Map mappings) { - List osiRelationships = getList(sourceData, RELATIONSHIPS); - if (osiRelationships == null) { + List ossieRelationships = getList(sourceData, RELATIONSHIPS); + if (ossieRelationships == null) { return; } // Validate and filter relationships - remove those with non-existent fields - List validRelationships = validateAndFilterRelationships(osiRelationships, outputData); + List validRelationships = validateAndFilterRelationships(ossieRelationships, outputData); if (validRelationships.isEmpty()) { return; } @@ -92,7 +92,7 @@ private void mapOsiToSalesforce( // Apply manual mappings (criteria) List sfRelationships = getList(outputData, SEMANTIC_RELATIONSHIPS); if (sfRelationships != null) { - reconstructCriteria(osiRelationships, sfRelationships); + reconstructCriteria(ossieRelationships, sfRelationships); } customExtensionHandler.restoreCustomExtensionsAtLevel(outputData, sourceData, Level.RELATIONSHIPS); @@ -105,7 +105,7 @@ private void mapOsiToSalesforce( /** * Maps Salesforce semanticRelationships to Ossie relationships. */ - private void mapSalesforceToOsi( + private void mapSalesforceToOssie( Map sourceData, Map outputData, Map mappings) { List sfRelationships = getList(sourceData, SEMANTIC_RELATIONSHIPS); @@ -153,9 +153,9 @@ private void mapSalesforceToOsi( outputData.putAll(mappedData); - List osiRelationships = getList(outputData, RELATIONSHIPS); - if (osiRelationships != null) { - deconstructCriteria(supportedRelationships, osiRelationships); + List ossieRelationships = getList(outputData, RELATIONSHIPS); + if (ossieRelationships != null) { + deconstructCriteria(supportedRelationships, ossieRelationships); } // Store unmapped SF properties in custom_extensions @@ -168,13 +168,13 @@ private void mapSalesforceToOsi( /** * Reconstructs criteria for Ossie→SF conversion. */ - private void reconstructCriteria(List osiRelationships, List sfRelationships) { - for (int i = 0; i < osiRelationships.size() && i < sfRelationships.size(); i++) { - Map osiRel = asMap(osiRelationships.get(i)); + private void reconstructCriteria(List ossieRelationships, List sfRelationships) { + for (int i = 0; i < ossieRelationships.size() && i < sfRelationships.size(); i++) { + Map ossieRel = asMap(ossieRelationships.get(i)); Map sfRel = asMap(sfRelationships.get(i)); - String fromEntity = getString(osiRel, FROM); - String toEntity = getString(osiRel, TO); + String fromEntity = getString(ossieRel, FROM); + String toEntity = getString(ossieRel, TO); if (fromEntity != null) { sfRel.put(LEFT_SEMANTIC_DEFINITION_API_NAME, fromEntity); @@ -183,8 +183,8 @@ private void reconstructCriteria(List osiRelationships, List sfR sfRel.put(RIGHT_SEMANTIC_DEFINITION_API_NAME, toEntity); } - Object fromColumnsObj = osiRel.get(FROM_COLUMNS); - Object toColumnsObj = osiRel.get(TO_COLUMNS); + Object fromColumnsObj = ossieRel.get(FROM_COLUMNS); + Object toColumnsObj = ossieRel.get(TO_COLUMNS); if (fromColumnsObj == null || toColumnsObj == null) { return; @@ -223,21 +223,21 @@ private void reconstructCriteria(List osiRelationships, List sfR * Note: This method only processes supported relationships (TableField types). * Unsupported relationships are filtered out earlier and stored in custom_extensions. */ - private void deconstructCriteria(List sfRelationships, List osiRelationships) { - for (int i = 0; i < sfRelationships.size() && i < osiRelationships.size(); i++) { + private void deconstructCriteria(List sfRelationships, List ossieRelationships) { + for (int i = 0; i < sfRelationships.size() && i < ossieRelationships.size(); i++) { Map sfRel = asMap(sfRelationships.get(i)); - Map osiRel = asMap(osiRelationships.get(i)); + Map ossieRel = asMap(ossieRelationships.get(i)); // Extract leftSemanticDefinitionApiName → from String leftDef = getString(sfRel, LEFT_SEMANTIC_DEFINITION_API_NAME); if (leftDef != null) { - osiRel.put(FROM, leftDef); + ossieRel.put(FROM, leftDef); } // Extract rightSemanticDefinitionApiName → to String rightDef = getString(sfRel, RIGHT_SEMANTIC_DEFINITION_API_NAME); if (rightDef != null) { - osiRel.put(TO, rightDef); + ossieRel.put(TO, rightDef); } Object criteriaObj = sfRel.get(CRITERIA); @@ -260,10 +260,10 @@ private void deconstructCriteria(List sfRelationships, List osiR } if (!fromColumns.isEmpty()) { - osiRel.put(FROM_COLUMNS, fromColumns); + ossieRel.put(FROM_COLUMNS, fromColumns); } if (!toColumns.isEmpty()) { - osiRel.put(TO_COLUMNS, toColumns); + ossieRel.put(TO_COLUMNS, toColumns); } } } @@ -286,19 +286,19 @@ private void applyDefaults(List sfRelationships) { /** * Validates and filters relationships, removing those that reference non-existent fields. (Calculated fields that are not supported) * - * @param osiRelationships List of Ossie relationships to validate + * @param ossieRelationships List of Ossie relationships to validate * @param outputData The output data containing semanticDataObjects with their fields * @return Filtered list of valid relationships */ - private List validateAndFilterRelationships(List osiRelationships, Map outputData) { + private List validateAndFilterRelationships(List ossieRelationships, Map outputData) { List validRelationships = new ArrayList<>(); List sfDataObjects = getList(outputData, SEMANTIC_DATA_OBJECTS); - for (Object relObj : osiRelationships) { - Map osiRel = asMap(relObj); - String relName = getString(osiRel, NAME); - String fromEntity = getString(osiRel, FROM); - String toEntity = getString(osiRel, TO); + for (Object relObj : ossieRelationships) { + Map ossieRel = asMap(relObj); + String relName = getString(ossieRel, NAME); + String fromEntity = getString(ossieRel, FROM); + String toEntity = getString(ossieRel, TO); Map fromDataObject = findDataObjectByName(sfDataObjects, fromEntity); Map toDataObject = findDataObjectByName(sfDataObjects, toEntity); @@ -308,14 +308,14 @@ private List validateAndFilterRelationships(List osiRelationship continue; } - List fromColumns = getList(osiRel, FROM_COLUMNS); - List toColumns = getList(osiRel, TO_COLUMNS); + List fromColumns = getList(ossieRel, FROM_COLUMNS); + List toColumns = getList(ossieRel, TO_COLUMNS); if (!validateColumns(fromColumns, fromDataObject, fromEntity, relName) || !validateColumns(toColumns, toDataObject, toEntity, relName)) { continue; } - validRelationships.add(osiRel); + validRelationships.add(ossieRel); } return validRelationships; } diff --git a/converters/salesforce/src/main/java/org/apache/ossie/converter/SalesforceDataTypeMapper.java b/converters/salesforce/src/main/java/org/apache/ossie/converter/SalesforceDataTypeMapper.java index 78a7258c..58e1835e 100644 --- a/converters/salesforce/src/main/java/org/apache/ossie/converter/SalesforceDataTypeMapper.java +++ b/converters/salesforce/src/main/java/org/apache/ossie/converter/SalesforceDataTypeMapper.java @@ -62,11 +62,11 @@ static String toOssie(String salesforceDataType) { } /** Maps an Ossie logical type to Salesforce, or returns null when no safe mapping exists. */ - static String toSalesforce(String osiDatatype) { - if (osiDatatype == null || osiDatatype.isBlank()) { + static String toSalesforce(String ossieDatatype) { + if (ossieDatatype == null || ossieDatatype.isBlank()) { return null; } - return OSSIE_TO_SALESFORCE.get(osiDatatype); + return OSSIE_TO_SALESFORCE.get(ossieDatatype); } static boolean isTemporalSalesforceType(String salesforceDataType) { @@ -74,8 +74,8 @@ static boolean isTemporalSalesforceType(String salesforceDataType) { } /** Returns whether this mapping loses Ossie's timezone-free DateTime distinction. */ - static boolean isTimezoneLossyMapping(String osiDatatype, String salesforceDataType) { - return "DateTime".equals(osiDatatype) && "DateTime".equals(salesforceDataType); + static boolean isTimezoneLossyMapping(String ossieDatatype, String salesforceDataType) { + return "DateTime".equals(ossieDatatype) && "DateTime".equals(salesforceDataType); } /** @@ -83,11 +83,11 @@ static boolean isTimezoneLossyMapping(String osiDatatype, String salesforceDataT * describe compatible values. This treats lossy portable collapses such as * Email/String and Currency/Decimal as compatible. */ - static boolean areCompatible(String osiDatatype, String salesforceDataType) { - if (osiDatatype == null || salesforceDataType == null) { + static boolean areCompatible(String ossieDatatype, String salesforceDataType) { + if (ossieDatatype == null || salesforceDataType == null) { return true; } - return Objects.equals(toSalesforce(osiDatatype), salesforceDataType) - || Objects.equals(toOssie(salesforceDataType), osiDatatype); + return Objects.equals(toSalesforce(ossieDatatype), salesforceDataType) + || Objects.equals(toOssie(salesforceDataType), ossieDatatype); } } diff --git a/converters/salesforce/src/main/java/org/apache/ossie/converter/SemanticModelMappingHandler.java b/converters/salesforce/src/main/java/org/apache/ossie/converter/SemanticModelMappingHandler.java index d6c997ce..c94138d2 100644 --- a/converters/salesforce/src/main/java/org/apache/ossie/converter/SemanticModelMappingHandler.java +++ b/converters/salesforce/src/main/java/org/apache/ossie/converter/SemanticModelMappingHandler.java @@ -60,10 +60,10 @@ public SemanticModelMappingHandler(ConversionDirection direction, CustomExtensio @Override public void execute(Map sourceData, Map outputData, Map mappings) { - if (direction == ConversionDirection.OSI_TO_SALESFORCE) { - mapOsiToSalesforce(sourceData, outputData, mappings); + if (direction == ConversionDirection.OSSIE_TO_SALESFORCE) { + mapOssieToSalesforce(sourceData, outputData, mappings); } else { - mapSalesforceToOsi(sourceData, outputData, mappings); + mapSalesforceToOssie(sourceData, outputData, mappings); } } @@ -71,7 +71,7 @@ public void execute(Map sourceData, Map outputDa * Maps Ossie top-level properties to Salesforce format. * Steps: generic mappings → manual conversions → restore custom extensions */ - private void mapOsiToSalesforce( + private void mapOssieToSalesforce( Map sourceData, Map outputData, Map mappings) { var mappedData = GenericMappingEngine.applyMappings(sourceData, mappings); @@ -88,7 +88,7 @@ private void mapOsiToSalesforce( * Maps Salesforce top-level properties to Ossie format. * Steps: generic mappings → manual conversions → store custom extensions */ - private void mapSalesforceToOsi( + private void mapSalesforceToOssie( Map sourceData, Map outputData, Map mappings) { var mappedData = GenericMappingEngine.applyMappings(sourceData, mappings); diff --git a/converters/salesforce/src/main/java/org/apache/ossie/converter/pipeline/PipelineConfigLoader.java b/converters/salesforce/src/main/java/org/apache/ossie/converter/pipeline/PipelineConfigLoader.java index 6cf19133..c1f0466f 100644 --- a/converters/salesforce/src/main/java/org/apache/ossie/converter/pipeline/PipelineConfigLoader.java +++ b/converters/salesforce/src/main/java/org/apache/ossie/converter/pipeline/PipelineConfigLoader.java @@ -32,11 +32,11 @@ /** * Loads pipeline configuration from YAML. - * Uses Jackson to deserialize osi-salesforce-converter-config.yaml into PipelineConfig model. + * Uses Jackson to deserialize ossie-salesforce-converter-config.yaml into PipelineConfig model. * */ public class PipelineConfigLoader { - private static final String CONFIG_RESOURCE = "/osi-salesforce-converter-config.yaml"; + private static final String CONFIG_RESOURCE = "/ossie-salesforce-converter-config.yaml"; private final ObjectMapper yamlMapper; public PipelineConfigLoader() { @@ -58,7 +58,7 @@ public static PipelineConfig loadFromResource() { PipelineConfig config = new PipelineConfig(); config.setPipelines((Map>) rawConfig.get("pipelines")); - // Parse direction configs (osiToSalesforce, salesforceToOsi sections) + // Parse direction configs (ossieToSalesforce, salesforceToOssie sections) Map directionConfigs = new HashMap<>(); for (String direction : config.getPipelines().keySet()) { if (rawConfig.containsKey(direction)) { diff --git a/converters/salesforce/src/main/java/org/apache/ossie/mapper/FileBasedPropertyMapper.java b/converters/salesforce/src/main/java/org/apache/ossie/mapper/FileBasedPropertyMapper.java index a40e6e02..1ad4b4f0 100644 --- a/converters/salesforce/src/main/java/org/apache/ossie/mapper/FileBasedPropertyMapper.java +++ b/converters/salesforce/src/main/java/org/apache/ossie/mapper/FileBasedPropertyMapper.java @@ -96,12 +96,12 @@ private static FileBasedPropertyMapper parseYaml(String content) { } @Override - public Map getOsiToSalesforceMappings() { + public Map getOssieToSalesforceMappings() { return Collections.unmodifiableMap(mappings); } @Override - public Map getSalesforceToOsiMappings() { + public Map getSalesforceToOssieMappings() { return Collections.unmodifiableMap(reverseMappings); } diff --git a/converters/salesforce/src/main/java/org/apache/ossie/mapper/PropertyMapper.java b/converters/salesforce/src/main/java/org/apache/ossie/mapper/PropertyMapper.java index 1c6ccf8c..f1b5ac29 100644 --- a/converters/salesforce/src/main/java/org/apache/ossie/mapper/PropertyMapper.java +++ b/converters/salesforce/src/main/java/org/apache/ossie/mapper/PropertyMapper.java @@ -37,13 +37,13 @@ public interface PropertyMapper { * * @return a map where keys are Ossie property paths and values are Salesforce property paths */ - Map getOsiToSalesforceMappings(); + Map getOssieToSalesforceMappings(); /** * Returns the mapping from Salesforce property paths to Ossie property paths. - *

This is the reverse mapping of {@link #getOsiToSalesforceMappings()}. + *

This is the reverse mapping of {@link #getOssieToSalesforceMappings()}. * * @return a map where keys are Salesforce property paths and values are Ossie property paths */ - Map getSalesforceToOsiMappings(); + Map getSalesforceToOssieMappings(); } diff --git a/converters/salesforce/src/main/java/org/apache/ossie/validator/SchemaValidator.java b/converters/salesforce/src/main/java/org/apache/ossie/validator/SchemaValidator.java index b1bea2c0..53c23ff1 100644 --- a/converters/salesforce/src/main/java/org/apache/ossie/validator/SchemaValidator.java +++ b/converters/salesforce/src/main/java/org/apache/ossie/validator/SchemaValidator.java @@ -44,7 +44,7 @@ public class SchemaValidator { private static final Logger logger = LoggerFactory.getLogger(SchemaValidator.class); - public static final String OSI_SCHEMA_PATH = "/schemas/osi-schema.json"; + public static final String OSSIE_SCHEMA_PATH = "/schemas/ossie-schema.json"; public static final String SALESFORCE_SCHEMA_PATH = "/schemas/salesforce-semantic-model-schema.json"; private final JsonSchema schema; diff --git a/converters/salesforce/src/main/resources/mappings.yaml b/converters/salesforce/src/main/resources/mappings.yaml index 15f70cb1..91796970 100644 --- a/converters/salesforce/src/main/resources/mappings.yaml +++ b/converters/salesforce/src/main/resources/mappings.yaml @@ -26,7 +26,7 @@ # by special handlers and are NOT listed here. # # Format: -# osiPath: salesforcePath +# ossiePath: salesforcePath # # ============================================================================= diff --git a/converters/salesforce/src/main/resources/osi-salesforce-converter-config.yaml b/converters/salesforce/src/main/resources/ossie-salesforce-converter-config.yaml similarity index 92% rename from converters/salesforce/src/main/resources/osi-salesforce-converter-config.yaml rename to converters/salesforce/src/main/resources/ossie-salesforce-converter-config.yaml index 371c90d8..28c1d499 100644 --- a/converters/salesforce/src/main/resources/osi-salesforce-converter-config.yaml +++ b/converters/salesforce/src/main/resources/ossie-salesforce-converter-config.yaml @@ -18,14 +18,14 @@ # Pipeline definitions - each direction lists handlers to execute in order pipelines: - osiToSalesforce: + ossieToSalesforce: - DatasetMappingHandler - FieldMappingHandler - RelationshipMappingHandler - MetricMappingHandler - SemanticModelMappingHandler - salesforceToOsi: + salesforceToOssie: - DatasetMappingHandler - FieldMappingHandler - RelationshipMappingHandler @@ -33,13 +33,13 @@ pipelines: - SemanticModelMappingHandler # Direction-specific configuration -osiToSalesforce: +ossieToSalesforce: inputFormat: yaml outputFormat: json - schemaPath: /schemas/osi-schema.json + schemaPath: /schemas/ossie-schema.json extractModelNameFrom: apiName -salesforceToOsi: +salesforceToOssie: inputFormat: json outputFormat: yaml schemaPath: /schemas/salesforce-semantic-model-schema.json diff --git a/converters/salesforce/src/test/java/org/apache/ossie/OsiToSalesforceConverterTest.java b/converters/salesforce/src/test/java/org/apache/ossie/OssieToSalesforceConverterTest.java similarity index 88% rename from converters/salesforce/src/test/java/org/apache/ossie/OsiToSalesforceConverterTest.java rename to converters/salesforce/src/test/java/org/apache/ossie/OssieToSalesforceConverterTest.java index c298df6b..6d5c1237 100644 --- a/converters/salesforce/src/test/java/org/apache/ossie/OsiToSalesforceConverterTest.java +++ b/converters/salesforce/src/test/java/org/apache/ossie/OssieToSalesforceConverterTest.java @@ -45,40 +45,40 @@ /** * Comprehensive integration test for Ossie → Salesforce conversion. - * Uses real example file: osiToSalesforce.yaml + * Uses real example file: ossieToSalesforce.yaml */ -class OsiToSalesforceConverterTest { +class OssieToSalesforceConverterTest { private static boolean salesforceSchemaExists; - private static boolean osiSchemaExists; + private static boolean ossieSchemaExists; private static boolean warningPrinted = false; private Converter converter; private ObjectMapper jsonMapper; - private String osiYaml; - private String osiYamlAnsiSql; + private String ossieYaml; + private String ossieYamlAnsiSql; @BeforeAll static void checkSchemaAvailability() { - salesforceSchemaExists = OsiToSalesforceConverterTest.class + salesforceSchemaExists = OssieToSalesforceConverterTest.class .getResourceAsStream(SchemaValidator.SALESFORCE_SCHEMA_PATH) != null; - osiSchemaExists = OsiToSalesforceConverterTest.class - .getResourceAsStream(SchemaValidator.OSI_SCHEMA_PATH) != null; + ossieSchemaExists = OssieToSalesforceConverterTest.class + .getResourceAsStream(SchemaValidator.OSSIE_SCHEMA_PATH) != null; if (!warningPrinted) { if (!salesforceSchemaExists) { System.err.println("\n WARNING: Salesforce schema not found at " + SchemaValidator.SALESFORCE_SCHEMA_PATH); - System.err.println(" Some tests in OsiToSalesforceConverterTest will be skipped."); + System.err.println(" Some tests in OssieToSalesforceConverterTest will be skipped."); System.err.println(" To run all tests, download the schema from:"); System.err.println(" https://developer.salesforce.com/docs/data/semantic-layer/guide/salesforce-semantic-model-schema.html"); System.err.println(" and save it to: src/main/resources/schemas/salesforce-semantic-model-schema.json\n"); } - if (!osiSchemaExists) { - System.err.println("\n WARNING: Ossie schema not found at " + SchemaValidator.OSI_SCHEMA_PATH); - System.err.println(" Skipping OsiToSalesforceConverterTest tests."); + if (!ossieSchemaExists) { + System.err.println("\n WARNING: Ossie schema not found at " + SchemaValidator.OSSIE_SCHEMA_PATH); + System.err.println(" Skipping OssieToSalesforceConverterTest tests."); System.err.println(" To run these tests, download the schema from:"); - System.err.println(" https://github.com/apache/ossie/blob/main/core-spec/osi-schema.json"); - System.err.println(" and save it to: src/main/resources/schemas/osi-schema.json\n"); + System.err.println(" https://github.com/apache/ossie/blob/main/core-spec/ossie-schema.json"); + System.err.println(" and save it to: src/main/resources/schemas/ossie-schema.json\n"); } warningPrinted = true; } @@ -86,17 +86,17 @@ static void checkSchemaAvailability() { @BeforeEach void setUp() throws IOException { - assumeTrue(osiSchemaExists, "Ossie schema file is required but not found. See README for setup instructions."); + assumeTrue(ossieSchemaExists, "Ossie schema file is required but not found. See README for setup instructions."); - converter = ConverterFactory.getConverter(ConversionDirection.OSI_TO_SALESFORCE); + converter = ConverterFactory.getConverter(ConversionDirection.OSSIE_TO_SALESFORCE); jsonMapper = new ObjectMapper(); - osiYamlAnsiSql = Files.readString(Paths.get("src/test/resources/examples/osiToSalesforce.yaml")); - osiYaml = osiYamlAnsiSql; + ossieYamlAnsiSql = Files.readString(Paths.get("src/test/resources/examples/ossieToSalesforce.yaml")); + ossieYaml = ossieYamlAnsiSql; } @Test void testCompleteConversion() throws Exception { - List results = converter.convert(osiYaml); + List results = converter.convert(ossieYaml); assertNotNull(results); assertEquals(1, results.size()); @@ -114,7 +114,7 @@ void testCompleteConversion() throws Exception { @Test void testDatasetMapping() throws Exception { - List results = converter.convert(osiYaml); + List results = converter.convert(ossieYaml); Map sfModel = jsonMapper.readValue(results.get(0), new TypeReference>() {}); List> dataObjects = (List>) sfModel.get("semanticDataObjects"); @@ -136,7 +136,7 @@ void testDatasetMapping() throws Exception { @Test void testFieldSplittingIntoDimensionsAndMeasurements() throws Exception { - List results = converter.convert(osiYaml); + List results = converter.convert(ossieYaml); Map sfModel = jsonMapper.readValue(results.get(0), new TypeReference>() {}); List> dataObjects = (List>) sfModel.get("semanticDataObjects"); @@ -170,7 +170,7 @@ void testFieldSplittingIntoDimensionsAndMeasurements() throws Exception { @Test void testExpressionUnwrappingFromDialects() throws Exception { - List results = converter.convert(osiYaml); + List results = converter.convert(ossieYaml); Map sfModel = jsonMapper.readValue(results.get(0), new TypeReference>() {}); List> dataObjects = (List>) sfModel.get("semanticDataObjects"); @@ -194,7 +194,7 @@ void testExpressionUnwrappingFromDialects() throws Exception { @Test void testRelationshipConversion() throws Exception { - List results = converter.convert(osiYaml); + List results = converter.convert(ossieYaml); Map sfModel = jsonMapper.readValue(results.get(0), new TypeReference>() {}); List> relationships = (List>) sfModel.get("semanticRelationships"); @@ -220,7 +220,7 @@ void testRelationshipConversion() throws Exception { @Test void testCalculatedFieldDetection() throws Exception { - List ansiResults = converter.convert(osiYamlAnsiSql); + List ansiResults = converter.convert(ossieYamlAnsiSql); Map ansiModel = jsonMapper.readValue(ansiResults.get(0), new TypeReference>() {}); List> ansiCalcDimensions = (List>) ansiModel.get("semanticCalculatedDimensions"); @@ -229,7 +229,7 @@ void testCalculatedFieldDetection() throws Exception { @Test void testInvalidRelationshipsFiltered() throws Exception { - List results = converter.convert(osiYaml); + List results = converter.convert(ossieYaml); Map sfModel = jsonMapper.readValue(results.get(0), new TypeReference>() {}); List> relationships = (List>) sfModel.get("semanticRelationships"); @@ -247,7 +247,7 @@ void testInvalidRelationshipsFiltered() throws Exception { @Test void testCustomExtensionsRestoration() throws Exception { - List results = converter.convert(osiYaml); + List results = converter.convert(ossieYaml); Map sfModel = jsonMapper.readValue(results.get(0), new TypeReference>() {}); assertEquals("Customer_Orders_Model", sfModel.get("label")); @@ -287,8 +287,8 @@ void testCustomExtensionsRestoration() throws Exception { } @Test - void testMetricsNotConvertedInOsiToSalesforce() throws Exception { - List results = converter.convert(osiYaml); + void testMetricsNotConvertedInOssieToSalesforce() throws Exception { + List results = converter.convert(ossieYaml); Map sfModel = jsonMapper.readValue(results.get(0), new TypeReference>() {}); List> calcMeasurements = (List>) sfModel.get("semanticCalculatedMeasurements"); @@ -297,7 +297,7 @@ void testMetricsNotConvertedInOsiToSalesforce() throws Exception { @Test void testTimeDimensionConversion() throws Exception { - List results = converter.convert(osiYaml); + List results = converter.convert(ossieYaml); Map sfModel = jsonMapper.readValue(results.get(0), new TypeReference>() {}); List> dataObjects = (List>) sfModel.get("semanticDataObjects"); @@ -317,7 +317,7 @@ void testTimeDimensionConversion() throws Exception { void testOutputCompilesWithSalesforceSchema() throws Exception { assumeTrue(salesforceSchemaExists, "Salesforce schema file is required but not found. See README for setup instructions."); - List results = converter.convert(osiYaml); + List results = converter.convert(ossieYaml); String salesforceJson = results.get(0); Map sfModel = jsonMapper.readValue(salesforceJson, new TypeReference>() {}); @@ -334,11 +334,11 @@ void testPipelineConfigLoadsSuccessfully() { assertNotNull(config); assertNotNull(config.getPipelines()); - assertTrue(config.getPipelines().containsKey("osiToSalesforce")); - assertTrue(config.getPipelines().containsKey("salesforceToOsi")); + assertTrue(config.getPipelines().containsKey("ossieToSalesforce")); + assertTrue(config.getPipelines().containsKey("salesforceToOssie")); - // Verify handler list for osiToSalesforce - List handlers = config.getPipelines().get("osiToSalesforce"); + // Verify handler list for ossieToSalesforce + List handlers = config.getPipelines().get("ossieToSalesforce"); assertNotNull(handlers); assertEquals(5, handlers.size()); assertTrue(handlers.contains("DatasetMappingHandler")); @@ -350,11 +350,11 @@ void testPipelineConfigLoadsSuccessfully() { // Verify direction config assertNotNull(config.getDirectionConfigs()); DirectionConfig dirConfig = - config.getDirectionConfigs().get("osiToSalesforce"); + config.getDirectionConfigs().get("ossieToSalesforce"); assertNotNull(dirConfig); assertEquals("yaml", dirConfig.getInputFormat()); assertEquals("json", dirConfig.getOutputFormat()); - assertEquals("/schemas/osi-schema.json", dirConfig.getSchemaPath()); + assertEquals("/schemas/ossie-schema.json", dirConfig.getSchemaPath()); assertEquals("apiName", dirConfig.getExtractModelNameFrom()); } @@ -366,7 +366,7 @@ void testHandlerFactoryCreatesAllHandlers() { HandlerFactory factory = new HandlerFactory(customExtensionHandler); - ConversionDirection direction = ConversionDirection.OSI_TO_SALESFORCE; + ConversionDirection direction = ConversionDirection.OSSIE_TO_SALESFORCE; assertDoesNotThrow(() -> factory.createHandler("DatasetMappingHandler", direction)); assertDoesNotThrow(() -> factory.createHandler("FieldMappingHandler", direction)); diff --git a/converters/salesforce/src/test/java/org/apache/ossie/SalesforceToOsiConverterTest.java b/converters/salesforce/src/test/java/org/apache/ossie/SalesforceToOssieConverterTest.java similarity index 82% rename from converters/salesforce/src/test/java/org/apache/ossie/SalesforceToOsiConverterTest.java rename to converters/salesforce/src/test/java/org/apache/ossie/SalesforceToOssieConverterTest.java index 2cdae4b0..ffc77647 100644 --- a/converters/salesforce/src/test/java/org/apache/ossie/SalesforceToOsiConverterTest.java +++ b/converters/salesforce/src/test/java/org/apache/ossie/SalesforceToOssieConverterTest.java @@ -42,12 +42,12 @@ /** * Comprehensive integration test for Salesforce → Ossie conversion. - * Uses real example file: salesforceToOsi.json + * Uses real example file: salesforceToOssie.json */ -class SalesforceToOsiConverterTest { +class SalesforceToOssieConverterTest { private static boolean schemaExists; - private static boolean osiSchemaExists; + private static boolean ossieSchemaExists; private static boolean warningPrinted = false; private Converter converter; @@ -56,25 +56,25 @@ class SalesforceToOsiConverterTest { @BeforeAll static void checkSchemaAvailability() { - schemaExists = SalesforceToOsiConverterTest.class + schemaExists = SalesforceToOssieConverterTest.class .getResourceAsStream(SchemaValidator.SALESFORCE_SCHEMA_PATH) != null; - osiSchemaExists = SalesforceToOsiConverterTest.class - .getResourceAsStream(SchemaValidator.OSI_SCHEMA_PATH) != null; + ossieSchemaExists = SalesforceToOssieConverterTest.class + .getResourceAsStream(SchemaValidator.OSSIE_SCHEMA_PATH) != null; if (!warningPrinted) { if (!schemaExists) { System.err.println("\n WARNING: Salesforce schema not found at " + SchemaValidator.SALESFORCE_SCHEMA_PATH); - System.err.println(" Skipping SalesforceToOsiConverterTest tests."); + System.err.println(" Skipping SalesforceToOssieConverterTest tests."); System.err.println(" To run these tests, download the schema from:"); System.err.println(" https://developer.salesforce.com/docs/data/semantic-layer/guide/salesforce-semantic-model-schema.html"); System.err.println(" and save it to: src/main/resources/schemas/salesforce-semantic-model-schema.json\n"); } - if (!osiSchemaExists) { - System.err.println("\n WARNING: Ossie schema not found at " + SchemaValidator.OSI_SCHEMA_PATH); - System.err.println(" Skipping SalesforceToOsiConverterTest tests."); + if (!ossieSchemaExists) { + System.err.println("\n WARNING: Ossie schema not found at " + SchemaValidator.OSSIE_SCHEMA_PATH); + System.err.println(" Skipping SalesforceToOssieConverterTest tests."); System.err.println(" To run these tests, download the schema from:"); - System.err.println(" https://github.com/apache/ossie/blob/main/core-spec/osi-schema.json"); - System.err.println(" and save it to: src/main/resources/schemas/osi-schema.json\n"); + System.err.println(" https://github.com/apache/ossie/blob/main/core-spec/ossie-schema.json"); + System.err.println(" and save it to: src/main/resources/schemas/ossie-schema.json\n"); } warningPrinted = true; } @@ -83,11 +83,11 @@ static void checkSchemaAvailability() { @BeforeEach void setUp() throws IOException { assumeTrue(schemaExists, "Salesforce schema file is required but not found. See README for setup instructions."); - assumeTrue(osiSchemaExists, "Ossie schema file is required but not found. See README for setup instructions."); + assumeTrue(ossieSchemaExists, "Ossie schema file is required but not found. See README for setup instructions."); - converter = ConverterFactory.getConverter(ConversionDirection.SALESFORCE_TO_OSI); + converter = ConverterFactory.getConverter(ConversionDirection.SALESFORCE_TO_OSSIE); yamlMapper = new ObjectMapper(new YAMLFactory()); - salesforceJson = Files.readString(Paths.get("src/test/resources/examples/salesforceToOsi.json")); + salesforceJson = Files.readString(Paths.get("src/test/resources/examples/salesforceToOssie.json")); } @Test @@ -97,16 +97,16 @@ void testCompleteConversion() throws Exception { assertNotNull(results); assertEquals(1, results.size()); - String osiYaml = results.get(0); - assertNotNull(osiYaml); - assertTrue(osiYaml.contains("version: 0.2.0.dev0")); - assertTrue(osiYaml.contains("semantic_model:")); + String ossieYaml = results.get(0); + assertNotNull(ossieYaml); + assertTrue(ossieYaml.contains("version: 0.2.0.dev0")); + assertTrue(ossieYaml.contains("semantic_model:")); - Map osiRoot = yamlMapper.readValue(osiYaml, Map.class); - assertNotNull(osiRoot); - assertEquals("0.2.0.dev0", osiRoot.get("version")); + Map ossieRoot = yamlMapper.readValue(ossieYaml, Map.class); + assertNotNull(ossieRoot); + assertEquals("0.2.0.dev0", ossieRoot.get("version")); - List> semanticModels = (List>) osiRoot.get("semantic_model"); + List> semanticModels = (List>) ossieRoot.get("semantic_model"); assertNotNull(semanticModels); assertEquals(1, semanticModels.size()); @@ -118,8 +118,8 @@ void testCompleteConversion() throws Exception { @Test void testDatasetMapping() throws Exception { List results = converter.convert(salesforceJson); - Map osiRoot = yamlMapper.readValue(results.get(0), Map.class); - Map model = ((List>) osiRoot.get("semantic_model")).get(0); + Map ossieRoot = yamlMapper.readValue(results.get(0), Map.class); + Map model = ((List>) ossieRoot.get("semantic_model")).get(0); List> datasets = (List>) model.get("datasets"); assertNotNull(datasets); @@ -139,8 +139,8 @@ void testDatasetMapping() throws Exception { @Test void testFieldMapping() throws Exception { List results = converter.convert(salesforceJson); - Map osiRoot = yamlMapper.readValue(results.get(0), Map.class); - Map model = ((List>) osiRoot.get("semantic_model")).get(0); + Map ossieRoot = yamlMapper.readValue(results.get(0), Map.class); + Map model = ((List>) ossieRoot.get("semantic_model")).get(0); List> datasets = (List>) model.get("datasets"); Map customersDataset = datasets.get(0); @@ -185,8 +185,8 @@ void testFieldMapping() throws Exception { @Test void testCalculatedDimensionConversion() throws Exception { List results = converter.convert(salesforceJson); - Map osiRoot = yamlMapper.readValue(results.get(0), Map.class); - Map model = ((List>) osiRoot.get("semantic_model")).get(0); + Map ossieRoot = yamlMapper.readValue(results.get(0), Map.class); + Map model = ((List>) ossieRoot.get("semantic_model")).get(0); List> datasets = (List>) model.get("datasets"); // customer_email_domain should be converted to a field in Customers dataset (single dependency) @@ -223,8 +223,8 @@ void testCalculatedDimensionConversion() throws Exception { @Test void testRelationshipMapping() throws Exception { List results = converter.convert(salesforceJson); - Map osiRoot = yamlMapper.readValue(results.get(0), Map.class); - Map model = ((List>) osiRoot.get("semantic_model")).get(0); + Map ossieRoot = yamlMapper.readValue(results.get(0), Map.class); + Map model = ((List>) ossieRoot.get("semantic_model")).get(0); List> relationships = (List>) model.get("relationships"); assertNotNull(relationships); @@ -249,8 +249,8 @@ void testRelationshipMapping() throws Exception { @Test void testUnsupportedRelationshipsInCustomExtensions() throws Exception { List results = converter.convert(salesforceJson); - Map osiRoot = yamlMapper.readValue(results.get(0), Map.class); - Map model = ((List>) osiRoot.get("semantic_model")).get(0); + Map ossieRoot = yamlMapper.readValue(results.get(0), Map.class); + Map model = ((List>) ossieRoot.get("semantic_model")).get(0); // Unsupported relationships (Formula/SemanticField) should be in custom_extensions List> customExtensions = (List>) model.get("custom_extensions"); @@ -270,8 +270,8 @@ void testUnsupportedRelationshipsInCustomExtensions() throws Exception { @Test void testMetricMapping() throws Exception { List results = converter.convert(salesforceJson); - Map osiRoot = yamlMapper.readValue(results.get(0), Map.class); - Map model = ((List>) osiRoot.get("semantic_model")).get(0); + Map ossieRoot = yamlMapper.readValue(results.get(0), Map.class); + Map model = ((List>) ossieRoot.get("semantic_model")).get(0); List> metrics = (List>) model.get("metrics"); assertNotNull(metrics); @@ -293,8 +293,8 @@ void testMetricMapping() throws Exception { @Test void testCustomExtensionsPreservation() throws Exception { List results = converter.convert(salesforceJson); - Map osiRoot = yamlMapper.readValue(results.get(0), Map.class); - Map model = ((List>) osiRoot.get("semantic_model")).get(0); + Map ossieRoot = yamlMapper.readValue(results.get(0), Map.class); + Map model = ((List>) ossieRoot.get("semantic_model")).get(0); // Check model-level custom_extensions List> customExtensions = (List>) model.get("custom_extensions"); @@ -317,8 +317,8 @@ void testCustomExtensionsPreservation() throws Exception { @Test void testTimeDimensionMapping() throws Exception { List results = converter.convert(salesforceJson); - Map osiRoot = yamlMapper.readValue(results.get(0), Map.class); - Map model = ((List>) osiRoot.get("semantic_model")).get(0); + Map ossieRoot = yamlMapper.readValue(results.get(0), Map.class); + Map model = ((List>) ossieRoot.get("semantic_model")).get(0); List> datasets = (List>) model.get("datasets"); Map ordersDataset = datasets.get(1); @@ -337,24 +337,24 @@ void testTimeDimensionMapping() throws Exception { } @Test - void testOutputCompilesWithOsiSchema() throws Exception { + void testOutputCompilesWithOssieSchema() throws Exception { List results = converter.convert(salesforceJson); - String osiYaml = results.get(0); + String ossieYaml = results.get(0); - Map osiRoot = yamlMapper.readValue(osiYaml, Map.class); + Map ossieRoot = yamlMapper.readValue(ossieYaml, Map.class); - SchemaValidator validator = new SchemaValidator(new ObjectMapper(), SchemaValidator.OSI_SCHEMA_PATH); - assertDoesNotThrow(() -> validator.validate(osiRoot), "Output should comply with Ossie schema"); + SchemaValidator validator = new SchemaValidator(new ObjectMapper(), SchemaValidator.OSSIE_SCHEMA_PATH); + assertDoesNotThrow(() -> validator.validate(ossieRoot), "Output should comply with Ossie schema"); } @Test - void testPipelineConfigForSalesforceToOsi() { - // Verify pipeline configuration for salesforceToOsi direction + void testPipelineConfigForSalesforceToOssie() { + // Verify pipeline configuration for salesforceToOssie direction PipelineConfig config = PipelineConfigLoader.loadFromResource(); - // Verify handler list for salesforceToOsi - List handlers = config.getPipelines().get("salesforceToOsi"); + // Verify handler list for salesforceToOssie + List handlers = config.getPipelines().get("salesforceToOssie"); assertNotNull(handlers); assertEquals(5, handlers.size()); assertTrue(handlers.contains("DatasetMappingHandler")); @@ -363,9 +363,9 @@ void testPipelineConfigForSalesforceToOsi() { assertTrue(handlers.contains("MetricMappingHandler")); assertTrue(handlers.contains("SemanticModelMappingHandler")); - // Verify direction config for salesforceToOsi + // Verify direction config for salesforceToOssie DirectionConfig dirConfig = - config.getDirectionConfigs().get("salesforceToOsi"); + config.getDirectionConfigs().get("salesforceToOssie"); assertNotNull(dirConfig); assertEquals("json", dirConfig.getInputFormat()); assertEquals("yaml", dirConfig.getOutputFormat()); @@ -375,15 +375,15 @@ void testPipelineConfigForSalesforceToOsi() { } @Test - void testHandlerFactoryForSalesforceToOsi() { - // Verify HandlerFactory creates handlers for salesforceToOsi direction + void testHandlerFactoryForSalesforceToOssie() { + // Verify HandlerFactory creates handlers for salesforceToOssie direction ObjectMapper testJsonMapper = new ObjectMapper(); CustomExtensionHandler customExtensionHandler = new CustomExtensionHandler(testJsonMapper); HandlerFactory factory = new HandlerFactory(customExtensionHandler); - ConversionDirection direction = ConversionDirection.SALESFORCE_TO_OSI; + ConversionDirection direction = ConversionDirection.SALESFORCE_TO_OSSIE; // Verify all handlers can be instantiated PipelineStep datasetHandler = @@ -408,17 +408,17 @@ void testHandlerFactoryForSalesforceToOsi() { } @Test - void testConverterImplExtractModelNameFromOsiFormat() throws Exception { + void testConverterImplExtractModelNameFromOssieFormat() throws Exception { // Test extractModelName specifically handles Ossie wrapped format List results = converter.convert(salesforceJson); - String osiYaml = results.get(0); + String ossieYaml = results.get(0); // The result is wrapped Ossie format - extractModelName should handle this - Map osiRoot = yamlMapper.readValue(osiYaml, Map.class); - assertTrue(osiRoot.containsKey("semantic_model")); + Map ossieRoot = yamlMapper.readValue(ossieYaml, Map.class); + assertTrue(ossieRoot.containsKey("semantic_model")); // Verify it's properly wrapped - List> models = (List>) osiRoot.get("semantic_model"); + List> models = (List>) ossieRoot.get("semantic_model"); assertNotNull(models); assertEquals(1, models.size()); assertEquals("Customer_Orders_Model", models.get(0).get("name")); diff --git a/converters/salesforce/src/test/java/org/apache/ossie/converter/SalesforceDataTypeMapperTest.java b/converters/salesforce/src/test/java/org/apache/ossie/converter/SalesforceDataTypeMapperTest.java index c49c3d43..52e4f0fd 100644 --- a/converters/salesforce/src/test/java/org/apache/ossie/converter/SalesforceDataTypeMapperTest.java +++ b/converters/salesforce/src/test/java/org/apache/ossie/converter/SalesforceDataTypeMapperTest.java @@ -31,8 +31,8 @@ class SalesforceDataTypeMapperTest { @ParameterizedTest @MethodSource("salesforceToOssieTypes") - void mapsSalesforceTypesToOssie(String salesforceType, String osiDatatype) { - assertEquals(osiDatatype, SalesforceDataTypeMapper.toOssie(salesforceType)); + void mapsSalesforceTypesToOssie(String salesforceType, String ossieDatatype) { + assertEquals(ossieDatatype, SalesforceDataTypeMapper.toOssie(salesforceType)); } static Stream salesforceToOssieTypes() { @@ -54,8 +54,8 @@ static Stream salesforceToOssieTypes() { @ParameterizedTest @MethodSource("ossieToSalesforceTypes") - void mapsOssieTypesToSalesforce(String osiDatatype, String salesforceType) { - assertEquals(salesforceType, SalesforceDataTypeMapper.toSalesforce(osiDatatype)); + void mapsOssieTypesToSalesforce(String ossieDatatype, String salesforceType) { + assertEquals(salesforceType, SalesforceDataTypeMapper.toSalesforce(ossieDatatype)); } static Stream ossieToSalesforceTypes() { diff --git a/converters/salesforce/src/test/resources/examples/osiToSalesforce.yaml b/converters/salesforce/src/test/resources/examples/ossieToSalesforce.yaml similarity index 100% rename from converters/salesforce/src/test/resources/examples/osiToSalesforce.yaml rename to converters/salesforce/src/test/resources/examples/ossieToSalesforce.yaml diff --git a/converters/salesforce/src/test/resources/examples/salesforceToOsi.json b/converters/salesforce/src/test/resources/examples/salesforceToOssie.json similarity index 100% rename from converters/salesforce/src/test/resources/examples/salesforceToOsi.json rename to converters/salesforce/src/test/resources/examples/salesforceToOssie.json diff --git a/converters/snowflake/src/ossie_snowflake/converter.py b/converters/snowflake/src/ossie_snowflake/converter.py index 6dcd77f3..d8588826 100644 --- a/converters/snowflake/src/ossie_snowflake/converter.py +++ b/converters/snowflake/src/ossie_snowflake/converter.py @@ -48,7 +48,7 @@ } -class OsiConversionError(Exception): +class OssieConversionError(Exception): """Raised when an Ossie YAML cannot be converted to Snowflake format.""" @@ -77,7 +77,7 @@ def _convert_datatype(datatype, field_name): return snowflake_datatype -def convert_osi_to_snowflake(osi_yaml_str): +def convert_ossie_to_snowflake(ossie_yaml_str): """Top-level entry point. Parses Ossie YAML, validates, converts, returns Snowflake YAML string. @@ -88,28 +88,28 @@ def convert_osi_to_snowflake(osi_yaml_str): - name: ... Args: - osi_yaml_str: Ossie YAML as a string. + ossie_yaml_str: Ossie YAML as a string. Returns: Snowflake Cortex Analyst semantic model YAML string. Raises: - OsiConversionError: If the input cannot be converted. + OssieConversionError: If the input cannot be converted. """ - root = yaml.safe_load(osi_yaml_str) + root = yaml.safe_load(ossie_yaml_str) if not isinstance(root, dict): - raise OsiConversionError("Invalid Ossie YAML: expected a mapping at the root") + raise OssieConversionError("Invalid Ossie YAML: expected a mapping at the root") version_str = str(root.get("version", "")) if version_str != SUPPORTED_VERSION: - raise OsiConversionError( + raise OssieConversionError( f"Unsupported Ossie specification version '{version_str}'. " f"Supported: {SUPPORTED_VERSION}" ) semantic_model = root.get("semantic_model") if not isinstance(semantic_model, list) or len(semantic_model) == 0: - raise OsiConversionError( + raise OssieConversionError( "Invalid Ossie YAML: 'semantic_model' must be a non-empty list" ) @@ -121,7 +121,7 @@ def convert_osi_to_snowflake(osi_yaml_str): ossie = semantic_model[0] if not isinstance(ossie, dict): - raise OsiConversionError( + raise OssieConversionError( "Invalid Ossie YAML: 'semantic_model' entries must be mappings" ) @@ -139,7 +139,7 @@ def _convert_model(ossie): """Converts the root Ossie model dict to a Snowflake semantic model dict.""" name = ossie.get("name") if not name: - raise OsiConversionError("Missing required 'name' field in semantic model") + raise OssieConversionError("Missing required 'name' field in semantic model") result = {} result["name"] = name @@ -187,7 +187,7 @@ def _convert_dataset(dataset): result = {} name = dataset.get("name") if not name: - raise OsiConversionError("Missing required 'name' field in dataset") + raise OssieConversionError("Missing required 'name' field in dataset") result["name"] = name # source -> base_table @@ -299,7 +299,7 @@ def _convert_named_expr(entry, kind): """ name = entry.get("name") if not name: - raise OsiConversionError(f"Missing required 'name' in {kind}") + raise OssieConversionError(f"Missing required 'name' in {kind}") expr_str = _extract_expression(entry.get("expression"), name) if expr_str is None: @@ -335,17 +335,17 @@ def _convert_relationship(rel): result = {} rel_name = rel.get("name") if not rel_name: - raise OsiConversionError("Missing required 'name' field in relationship") + raise OssieConversionError("Missing required 'name' field in relationship") result["name"] = rel_name left_table = rel.get("from") if not left_table: - raise OsiConversionError( + raise OssieConversionError( f"Relationship '{rel_name}': missing required 'from' field" ) right_table = rel.get("to") if not right_table: - raise OsiConversionError( + raise OssieConversionError( f"Relationship '{rel_name}': missing required 'to' field" ) result["left_table"] = left_table @@ -355,7 +355,7 @@ def _convert_relationship(rel): to_cols = rel.get("to_columns", []) if len(from_cols) != len(to_cols): - raise OsiConversionError( + raise OssieConversionError( f"Relationship '{rel_name}': from_columns and to_columns must have the " f"same length (got {len(from_cols)} and {len(to_cols)})" ) @@ -377,17 +377,17 @@ def _extract_expression(expression, field_name): """Selects the best dialect expression for Snowflake. Returns the expression string, or None if only unsupported dialects are - present (the field should be skipped). Raises OsiConversionError if the + present (the field should be skipped). Raises OssieConversionError if the expression or dialects list is missing entirely. """ if expression is None or not isinstance(expression, dict): - raise OsiConversionError( + raise OssieConversionError( f"Missing or malformed expression for field/metric '{field_name}'" ) dialects = expression.get("dialects") if not dialects: - raise OsiConversionError( + raise OssieConversionError( f"Missing expression for field/metric '{field_name}'" ) @@ -467,7 +467,7 @@ def _parse_source(source): "table": _normalize_identifier(parts[2]), } - raise OsiConversionError( + raise OssieConversionError( f"Source '{source}' must be a fully qualified db.schema.table or a subquery" ) @@ -528,11 +528,11 @@ def main(): args = parser.parse_args() with open(args.input, "r") as f: - osi_yaml_str = f.read() + ossie_yaml_str = f.read() try: - snowflake_yaml_str = convert_osi_to_snowflake(osi_yaml_str) - except OsiConversionError as e: + snowflake_yaml_str = convert_ossie_to_snowflake(ossie_yaml_str) + except OssieConversionError as e: print(f"Error: {e}", file=sys.stderr) sys.exit(1) diff --git a/converters/snowflake/tests/test_osi_to_snowflake_yaml_converter.py b/converters/snowflake/tests/test_ossie_to_snowflake_yaml_converter.py similarity index 90% rename from converters/snowflake/tests/test_osi_to_snowflake_yaml_converter.py rename to converters/snowflake/tests/test_ossie_to_snowflake_yaml_converter.py index 738fdf1f..bcc59fd4 100644 --- a/converters/snowflake/tests/test_osi_to_snowflake_yaml_converter.py +++ b/converters/snowflake/tests/test_ossie_to_snowflake_yaml_converter.py @@ -23,8 +23,8 @@ import yaml from ossie_snowflake.converter import ( - OsiConversionError, - convert_osi_to_snowflake, + OssieConversionError, + convert_ossie_to_snowflake, _classify_field, _convert_datatype, _convert_dataset, @@ -42,7 +42,7 @@ # Helpers # --------------------------------------------------------------------------- -def _wrap_osi(model_dict): +def _wrap_ossie(model_dict): """Wrap a model dict in the standard Ossie envelope.""" return yaml.dump( {"version": "0.2.0.dev0", "semantic_model": [model_dict]}, @@ -152,16 +152,16 @@ def test_whitespace_only_returns_none(self): assert _parse_source(" ") is None def test_two_part_name_raises(self): - with pytest.raises(OsiConversionError, match="fully qualified"): + with pytest.raises(OssieConversionError, match="fully qualified"): _parse_source("schema.table") def test_one_part_name_raises(self): - with pytest.raises(OsiConversionError, match="fully qualified"): + with pytest.raises(OssieConversionError, match="fully qualified"): _parse_source("table") def test_table_starting_with_select_not_subquery(self): """Table names like SELECT_RESULTS shouldn't be treated as subqueries.""" - with pytest.raises(OsiConversionError, match="fully qualified"): + with pytest.raises(OssieConversionError, match="fully qualified"): _parse_source("SELECT_RESULTS") @@ -198,7 +198,7 @@ def test_returns_copy(self): class TestConvertDatatype: @pytest.mark.parametrize( - ("osi_datatype", "snowflake_datatype"), + ("ossie_datatype", "snowflake_datatype"), [ ("String", "VARCHAR"), ("Integer", "NUMBER(38,0)"), @@ -211,8 +211,8 @@ class TestConvertDatatype: ("DateTimeTz", "TIMESTAMP_TZ"), ], ) - def test_maps_portable_datatype(self, osi_datatype, snowflake_datatype): - assert _convert_datatype(osi_datatype, "field") == snowflake_datatype + def test_maps_portable_datatype(self, ossie_datatype, snowflake_datatype): + assert _convert_datatype(ossie_datatype, "field") == snowflake_datatype def test_missing_datatype_is_omitted_without_warning(self): with warnings.catch_warnings(record=True) as caught: @@ -302,15 +302,15 @@ def test_unsupported_dialect_returns_none_with_warning(self): assert "my_field" in str(w[0].message) def test_missing_expression_raises(self): - with pytest.raises(OsiConversionError, match="Missing or malformed"): + with pytest.raises(OssieConversionError, match="Missing or malformed"): _extract_expression(None, "f") def test_missing_dialects_raises(self): - with pytest.raises(OsiConversionError, match="Missing expression"): + with pytest.raises(OssieConversionError, match="Missing expression"): _extract_expression({"dialects": []}, "f") def test_empty_dialects_raises(self): - with pytest.raises(OsiConversionError, match="Missing expression"): + with pytest.raises(OssieConversionError, match="Missing expression"): _extract_expression({"dialects": None}, "f") @@ -337,7 +337,7 @@ def test_basic_conversion(self): } def test_missing_name_raises(self): - with pytest.raises(OsiConversionError, match="Missing required 'name'"): + with pytest.raises(OssieConversionError, match="Missing required 'name'"): _convert_named_expr({"expression": {"dialects": []}}, "field") def test_no_description_or_synonyms(self): @@ -398,15 +398,15 @@ def test_multi_column_relationship(self): assert len(result["relationship_columns"]) == 2 def test_missing_name_raises(self): - with pytest.raises(OsiConversionError, match="Missing required 'name'"): + with pytest.raises(OssieConversionError, match="Missing required 'name'"): _convert_relationship({"from": "a", "to": "b"}) def test_missing_from_raises(self): - with pytest.raises(OsiConversionError, match="missing required 'from'"): + with pytest.raises(OssieConversionError, match="missing required 'from'"): _convert_relationship({"name": "r", "to": "b"}) def test_missing_to_raises(self): - with pytest.raises(OsiConversionError, match="missing required 'to'"): + with pytest.raises(OssieConversionError, match="missing required 'to'"): _convert_relationship({"name": "r", "from": "a"}) def test_column_count_mismatch_raises(self): @@ -417,7 +417,7 @@ def test_column_count_mismatch_raises(self): "from_columns": ["x"], "to_columns": ["p", "q"], } - with pytest.raises(OsiConversionError, match="same length"): + with pytest.raises(OssieConversionError, match="same length"): _convert_relationship(rel) def test_no_columns(self): @@ -521,7 +521,7 @@ def test_opaque_field_omits_data_type(self): assert "data_type" not in result["facts"][0] def test_missing_name_raises(self): - with pytest.raises(OsiConversionError, match="Missing required 'name'"): + with pytest.raises(OssieConversionError, match="Missing required 'name'"): _convert_dataset({"source": "db.s.t"}) def test_no_source(self): @@ -549,20 +549,20 @@ def test_no_fields(self): # --------------------------------------------------------------------------- -# convert_osi_to_snowflake (end-to-end) +# convert_ossie_to_snowflake (end-to-end) # --------------------------------------------------------------------------- -class TestConvertOsiToSnowflake: +class TestConvertOssieToSnowflake: def test_minimal_model(self): - osi_yaml = _wrap_osi(_minimal_model()) - result = yaml.safe_load(convert_osi_to_snowflake(osi_yaml)) + ossie_yaml = _wrap_ossie(_minimal_model()) + result = yaml.safe_load(convert_ossie_to_snowflake(ossie_yaml)) assert result["name"] == "test_model" assert "tables" in result assert result["tables"][0]["name"] == "my_table" def test_model_with_description(self): - osi_yaml = _wrap_osi(_minimal_model(description="A model")) - result = yaml.safe_load(convert_osi_to_snowflake(osi_yaml)) + ossie_yaml = _wrap_ossie(_minimal_model(description="A model")) + result = yaml.safe_load(convert_ossie_to_snowflake(ossie_yaml)) assert result["description"] == "A model" def test_model_with_relationships(self): @@ -577,7 +577,7 @@ def test_model_with_relationships(self): } ] ) - result = yaml.safe_load(convert_osi_to_snowflake(_wrap_osi(model))) + result = yaml.safe_load(convert_ossie_to_snowflake(_wrap_ossie(model))) assert len(result["relationships"]) == 1 assert result["relationships"][0]["name"] == "r1" @@ -596,39 +596,39 @@ def test_model_with_metrics(self): } ] ) - result = yaml.safe_load(convert_osi_to_snowflake(_wrap_osi(model))) + result = yaml.safe_load(convert_ossie_to_snowflake(_wrap_ossie(model))) assert len(result["metrics"]) == 1 assert result["metrics"][0]["expr"] == "SUM(x)" assert "data_type" not in result["metrics"][0] def test_invalid_yaml_root_raises(self): - with pytest.raises(OsiConversionError, match="expected a mapping"): - convert_osi_to_snowflake("- a list") + with pytest.raises(OssieConversionError, match="expected a mapping"): + convert_ossie_to_snowflake("- a list") def test_wrong_version_raises(self): bad = yaml.dump({"version": "9.9.9", "semantic_model": [{"name": "m"}]}) - with pytest.raises(OsiConversionError, match="Unsupported Ossie specification"): - convert_osi_to_snowflake(bad) + with pytest.raises(OssieConversionError, match="Unsupported Ossie specification"): + convert_ossie_to_snowflake(bad) def test_missing_semantic_model_raises(self): bad = yaml.dump({"version": "0.2.0.dev0"}) - with pytest.raises(OsiConversionError, match="non-empty list"): - convert_osi_to_snowflake(bad) + with pytest.raises(OssieConversionError, match="non-empty list"): + convert_ossie_to_snowflake(bad) def test_empty_semantic_model_raises(self): bad = yaml.dump({"version": "0.2.0.dev0", "semantic_model": []}) - with pytest.raises(OsiConversionError, match="non-empty list"): - convert_osi_to_snowflake(bad) + with pytest.raises(OssieConversionError, match="non-empty list"): + convert_ossie_to_snowflake(bad) def test_non_dict_model_entry_raises(self): bad = yaml.dump({"version": "0.2.0.dev0", "semantic_model": ["not a dict"]}) - with pytest.raises(OsiConversionError, match="must be mappings"): - convert_osi_to_snowflake(bad) + with pytest.raises(OssieConversionError, match="must be mappings"): + convert_ossie_to_snowflake(bad) def test_missing_model_name_raises(self): bad = yaml.dump({"version": "0.2.0.dev0", "semantic_model": [{"description": "x"}]}) - with pytest.raises(OsiConversionError, match="Missing required 'name'"): - convert_osi_to_snowflake(bad) + with pytest.raises(OssieConversionError, match="Missing required 'name'"): + convert_ossie_to_snowflake(bad) def test_multiple_models_warns(self): multi = yaml.dump( @@ -642,7 +642,7 @@ def test_multiple_models_warns(self): ) with warnings.catch_warnings(record=True) as w: warnings.simplefilter("always") - result = yaml.safe_load(convert_osi_to_snowflake(multi)) + result = yaml.safe_load(convert_ossie_to_snowflake(multi)) assert result["name"] == "first" assert any("only the first" in str(warning.message) for warning in w) @@ -668,7 +668,7 @@ def test_snowflake_dialect_preferred(self): } ], } - result = yaml.safe_load(convert_osi_to_snowflake(_wrap_osi(model))) + result = yaml.safe_load(convert_ossie_to_snowflake(_wrap_ossie(model))) assert result["tables"][0]["dimensions"][0]["expr"] == "snow_c" def test_fields_with_unsupported_dialect_skipped(self): @@ -703,7 +703,7 @@ def test_fields_with_unsupported_dialect_skipped(self): } with warnings.catch_warnings(record=True): warnings.simplefilter("always") - result = yaml.safe_load(convert_osi_to_snowflake(_wrap_osi(model))) + result = yaml.safe_load(convert_ossie_to_snowflake(_wrap_ossie(model))) dims = result["tables"][0]["dimensions"] assert len(dims) == 1 assert dims[0]["name"] == "good" @@ -729,7 +729,7 @@ def test_subquery_source(self): } ], } - result = yaml.safe_load(convert_osi_to_snowflake(_wrap_osi(model))) + result = yaml.safe_load(convert_ossie_to_snowflake(_wrap_ossie(model))) assert "definition" in result["tables"][0]["base_table"] @@ -811,7 +811,7 @@ def test_custom_extensions_dropped_with_warning(self): ) with warnings.catch_warnings(record=True) as w: warnings.simplefilter("always") - result = yaml.safe_load(convert_osi_to_snowflake(_wrap_osi(model))) + result = yaml.safe_load(convert_ossie_to_snowflake(_wrap_ossie(model))) assert "custom_extensions" not in result assert any("custom_extensions" in str(x.message) for x in w) @@ -821,7 +821,7 @@ def test_model_level_ai_context_dropped_with_warning(self): ) with warnings.catch_warnings(record=True) as w: warnings.simplefilter("always") - result = yaml.safe_load(convert_osi_to_snowflake(_wrap_osi(model))) + result = yaml.safe_load(convert_ossie_to_snowflake(_wrap_ossie(model))) assert "ai_context" not in result assert any("ai_context" in str(x.message) for x in w) @@ -840,7 +840,7 @@ def test_relationship_ai_context_dropped_with_warning(self): ) with warnings.catch_warnings(record=True) as w: warnings.simplefilter("always") - result = yaml.safe_load(convert_osi_to_snowflake(_wrap_osi(model))) + result = yaml.safe_load(convert_ossie_to_snowflake(_wrap_ossie(model))) rel = result["relationships"][0] assert "ai_context" not in rel assert "synonyms" not in rel @@ -853,7 +853,7 @@ def test_model_string_ai_context_appended_to_description(self): ) with warnings.catch_warnings(record=True) as w: warnings.simplefilter("always") - result = yaml.safe_load(convert_osi_to_snowflake(_wrap_osi(model))) + result = yaml.safe_load(convert_ossie_to_snowflake(_wrap_ossie(model))) assert result["description"] == "A model\nuse this model for analytics" assert not any("ai_context" in str(x.message) for x in w) @@ -861,7 +861,7 @@ def test_model_string_ai_context_becomes_description_when_none(self): model = _minimal_model(ai_context="use this model for analytics") with warnings.catch_warnings(record=True) as w: warnings.simplefilter("always") - result = yaml.safe_load(convert_osi_to_snowflake(_wrap_osi(model))) + result = yaml.safe_load(convert_ossie_to_snowflake(_wrap_ossie(model))) assert result["description"] == "use this model for analytics" assert not any("ai_context" in str(x.message) for x in w) @@ -890,7 +890,7 @@ def test_dataset_string_ai_context_appended_to_description(self): } with warnings.catch_warnings(record=True) as w: warnings.simplefilter("always") - result = yaml.safe_load(convert_osi_to_snowflake(_wrap_osi(model))) + result = yaml.safe_load(convert_ossie_to_snowflake(_wrap_ossie(model))) assert result["tables"][0]["description"] == "A table\ncontains sales data" assert not any("ai_context" in str(x.message) for x in w) @@ -919,7 +919,7 @@ def test_field_string_ai_context_appended_to_description(self): } with warnings.catch_warnings(record=True) as w: warnings.simplefilter("always") - result = yaml.safe_load(convert_osi_to_snowflake(_wrap_osi(model))) + result = yaml.safe_load(convert_ossie_to_snowflake(_wrap_ossie(model))) dim = result["tables"][0]["dimensions"][0] assert dim["description"] == "A column\nalways filter on this" assert not any("ai_context" in str(x.message) for x in w) @@ -939,7 +939,7 @@ def test_relationship_string_ai_context_dropped_with_warning(self): ) with warnings.catch_warnings(record=True) as w: warnings.simplefilter("always") - result = yaml.safe_load(convert_osi_to_snowflake(_wrap_osi(model))) + result = yaml.safe_load(convert_ossie_to_snowflake(_wrap_ossie(model))) rel = result["relationships"][0] assert "description" not in rel assert any("ai_context" in str(x.message) for x in w) @@ -968,7 +968,7 @@ def test_field_label_dropped_with_warning(self): } with warnings.catch_warnings(record=True) as w: warnings.simplefilter("always") - result = yaml.safe_load(convert_osi_to_snowflake(_wrap_osi(model))) + result = yaml.safe_load(convert_ossie_to_snowflake(_wrap_ossie(model))) dim = result["tables"][0]["dimensions"][0] assert "label" not in dim assert any("label" in str(x.message) for x in w) diff --git a/converters/wisdom/README.md b/converters/wisdom/README.md index c5884aad..1e1aba13 100644 --- a/converters/wisdom/README.md +++ b/converters/wisdom/README.md @@ -27,15 +27,15 @@ YAML document. ```bash pip install -e ../../python -e . -ossie-wisdom wisdom-to-osi -i domain-export.json -o semantic_model.yaml -ossie-wisdom osi-to-wisdom -i semantic_model.yaml -o domain-export.json +ossie-wisdom wisdom-to-ossie -i domain-export.json -o semantic_model.yaml +ossie-wisdom ossie-to-wisdom -i semantic_model.yaml -o domain-export.json ``` Conversion warnings (information loss) are printed to stderr; the output YAML validates -against the [Ossie JSON Schema](../../core-spec/osi-schema.json): +against the [Ossie JSON Schema](../../core-spec/ossie-schema.json): ```bash -python ../../validation/validate.py semantic_model.yaml --schema ../../core-spec/osi-schema.json +python ../../validation/validate.py semantic_model.yaml --schema ../../core-spec/ossie-schema.json ``` ## Field mapping @@ -77,7 +77,7 @@ drops the relationship with a `RELATIONSHIP_DROPPED` warning. ## Ossie → wisdom (export direction) -`osi-to-wisdom` emits a domain export mirroring wisdom's `exportDomain` JSON, inverting +`ossie-to-wisdom` emits a domain export mirroring wisdom's `exportDomain` JSON, inverting the mapping above so that wisdom → Ossie → wisdom and Ossie → wisdom → Ossie round-trips are stable: @@ -87,7 +87,7 @@ are stable: anything else becomes a formula. `dimension.is_time` becomes a `TIMESTAMP` data type (wisdom re-derives exact types from the warehouse). - Relationships become `MANY_TO_ONE` edges (Ossie's `from` is the many side); the - `ai_context` notes written by `wisdom-to-osi` restore `ONE_TO_ONE`/`MANY_TO_MANY`, and + `ai_context` notes written by `wisdom-to-ossie` restore `ONE_TO_ONE`/`MANY_TO_MANY`, and composite keys become compound `AND` join conditions. - Metrics attach to the first dataset their expression references (a `METRIC_TABLE_UNRESOLVED` warning falls back to the first dataset). diff --git a/converters/wisdom/src/ossie_wisdom/__init__.py b/converters/wisdom/src/ossie_wisdom/__init__.py index 337704d3..0d93a62e 100644 --- a/converters/wisdom/src/ossie_wisdom/__init__.py +++ b/converters/wisdom/src/ossie_wisdom/__init__.py @@ -16,13 +16,13 @@ # under the License. from ossie_wisdom.converter_issues import ConverterIssue, ConverterIssueType, ConverterResult -from ossie_wisdom.osi_to_wisdom import OSIToWisdomConverter -from ossie_wisdom.wisdom_to_osi import WisdomToOSIConverter +from ossie_wisdom.ossie_to_wisdom import OssieToWisdomConverter +from ossie_wisdom.wisdom_to_ossie import WisdomToOssieConverter __all__ = [ "ConverterIssue", "ConverterIssueType", "ConverterResult", - "OSIToWisdomConverter", - "WisdomToOSIConverter", + "OssieToWisdomConverter", + "WisdomToOssieConverter", ] diff --git a/converters/wisdom/src/ossie_wisdom/cli.py b/converters/wisdom/src/ossie_wisdom/cli.py index b4c93267..0f15423b 100644 --- a/converters/wisdom/src/ossie_wisdom/cli.py +++ b/converters/wisdom/src/ossie_wisdom/cli.py @@ -18,8 +18,8 @@ """CLI entry point for the ossie-wisdom converter. Usage: - ossie-wisdom wisdom-to-osi -i domain-export.json -o output.yaml - ossie-wisdom osi-to-wisdom -i input.yaml -o domain-export.json + ossie-wisdom wisdom-to-ossie -i domain-export.json -o output.yaml + ossie-wisdom ossie-to-wisdom -i input.yaml -o domain-export.json """ import argparse @@ -29,10 +29,10 @@ import yaml -from ossie import OSIDocument +from ossie import OssieDocument from ossie_wisdom.converter_issues import ConverterIssueType -from ossie_wisdom.osi_to_wisdom import OSIToWisdomConverter -from ossie_wisdom.wisdom_to_osi import WisdomToOSIConverter +from ossie_wisdom.ossie_to_wisdom import OssieToWisdomConverter +from ossie_wisdom.wisdom_to_ossie import WisdomToOssieConverter _ISSUE_REASON: dict[ConverterIssueType, str] = { ConverterIssueType.UNSUPPORTED_DIALECT: "the connection dialect has no Ossie equivalent; expressions were emitted verbatim as ANSI_SQL", @@ -66,24 +66,24 @@ def _print_issues(result) -> None: print(f"[WARNING] {issue.issue_type.value}: {issue.element_name} {verb} during conversion because {reason}", file=sys.stderr) -def _cmd_wisdom_to_osi(args: argparse.Namespace) -> None: +def _cmd_wisdom_to_ossie(args: argparse.Namespace) -> None: input_path = Path(args.input) output_path = Path(args.output) export = json.loads(input_path.read_text()) - result = WisdomToOSIConverter().convert(export) + result = WisdomToOssieConverter().convert(export) _print_issues(result) - output_path.write_text(result.output.to_osi_yaml()) + output_path.write_text(result.output.to_ossie_yaml()) print(f"Written to {output_path}", file=sys.stderr) -def _cmd_osi_to_wisdom(args: argparse.Namespace) -> None: +def _cmd_ossie_to_wisdom(args: argparse.Namespace) -> None: input_path = Path(args.input) output_path = Path(args.output) - document = OSIDocument.model_validate(yaml.safe_load(input_path.read_text())) - result = OSIToWisdomConverter().convert(document) + document = OssieDocument.model_validate(yaml.safe_load(input_path.read_text())) + result = OssieToWisdomConverter().convert(document) _print_issues(result) output_path.write_text(json.dumps(result.output, indent=2) + "\n") @@ -97,19 +97,19 @@ def main() -> None: ) subparsers = parser.add_subparsers(dest="command", required=True) - wisdom_to_osi = subparsers.add_parser("wisdom-to-osi", help="Convert domain-export.json → Ossie YAML") - wisdom_to_osi.add_argument("-i", "--input", required=True, metavar="FILE", help="Path to wisdom domain export JSON") - wisdom_to_osi.add_argument("-o", "--output", required=True, metavar="FILE", help="Path for output Ossie YAML") + wisdom_to_ossie = subparsers.add_parser("wisdom-to-ossie", help="Convert domain-export.json → Ossie YAML") + wisdom_to_ossie.add_argument("-i", "--input", required=True, metavar="FILE", help="Path to wisdom domain export JSON") + wisdom_to_ossie.add_argument("-o", "--output", required=True, metavar="FILE", help="Path for output Ossie YAML") - osi_to_wisdom = subparsers.add_parser("osi-to-wisdom", help="Convert Ossie YAML → domain-export.json") - osi_to_wisdom.add_argument("-i", "--input", required=True, metavar="FILE", help="Path to Ossie YAML") - osi_to_wisdom.add_argument("-o", "--output", required=True, metavar="FILE", help="Path for output wisdom domain export JSON") + ossie_to_wisdom = subparsers.add_parser("ossie-to-wisdom", help="Convert Ossie YAML → domain-export.json") + ossie_to_wisdom.add_argument("-i", "--input", required=True, metavar="FILE", help="Path to Ossie YAML") + ossie_to_wisdom.add_argument("-o", "--output", required=True, metavar="FILE", help="Path for output wisdom domain export JSON") args = parser.parse_args() - if args.command == "wisdom-to-osi": - _cmd_wisdom_to_osi(args) - elif args.command == "osi-to-wisdom": - _cmd_osi_to_wisdom(args) + if args.command == "wisdom-to-ossie": + _cmd_wisdom_to_ossie(args) + elif args.command == "ossie-to-wisdom": + _cmd_ossie_to_wisdom(args) if __name__ == "__main__": diff --git a/converters/wisdom/src/ossie_wisdom/osi_to_wisdom.py b/converters/wisdom/src/ossie_wisdom/ossie_to_wisdom.py similarity index 91% rename from converters/wisdom/src/ossie_wisdom/osi_to_wisdom.py rename to converters/wisdom/src/ossie_wisdom/ossie_to_wisdom.py index 0ae111fa..55524cda 100644 --- a/converters/wisdom/src/ossie_wisdom/osi_to_wisdom.py +++ b/converters/wisdom/src/ossie_wisdom/ossie_to_wisdom.py @@ -18,7 +18,7 @@ """Converts an Ossie Document into a WisdomAI domain export (format 1.0). The output mirrors the JSON produced by wisdom's ``exportDomain`` RPC so it -can be fed to ``importDomain``. Inverse of :mod:`ossie_wisdom.wisdom_to_osi`: +can be fed to ``importDomain``. Inverse of :mod:`ossie_wisdom.wisdom_to_ossie`: model ``ai_context`` splits back into system instructions and knowledge items, fields split back into columns and formulas, relationship direction is read as many-to-one (with the ai_context notes restoring one-to-one/many-to-many), and @@ -34,20 +34,20 @@ from typing import Dict, List, Optional, Tuple from ossie import ( - OSIAIContextObject, - OSIDataset, - OSIDialect, - OSIDocument, - OSIExpression, - OSISemanticModel, + OssieAIContextObject, + OssieDataset, + OssieDialect, + OssieDocument, + OssieExpression, + OssieSemanticModel, ) from ossie_wisdom.converter_issues import ConverterIssue, ConverterIssueType, ConverterResult _WISDOM_DIALECT = { - OSIDialect.SNOWFLAKE: "snowflake", - OSIDialect.DATABRICKS: "databricks", - OSIDialect.BIGQUERY: "bigquery", - OSIDialect.ANSI_SQL: "ansi", + OssieDialect.SNOWFLAKE: "snowflake", + OssieDialect.DATABRICKS: "databricks", + OssieDialect.BIGQUERY: "bigquery", + OssieDialect.ANSI_SQL: "ansi", } @@ -56,10 +56,10 @@ def _stable_id(prefix: str, value: str) -> str: return f"{prefix}_{hashlib.md5(value.encode('utf-8'), usedforsecurity=False).hexdigest()}" -class OSIToWisdomConverter: +class OssieToWisdomConverter: """Converts an Ossie Document into a wisdom domain export dict.""" - def convert(self, document: OSIDocument, exported_at: Optional[str] = None) -> ConverterResult[dict]: + def convert(self, document: OssieDocument, exported_at: Optional[str] = None) -> ConverterResult[dict]: issues: List[ConverterIssue] = [] model = document.semantic_model[0] @@ -131,7 +131,7 @@ def convert(self, document: OSIDocument, exported_at: Optional[str] = None) -> C } return ConverterResult(output=export, issues=issues) - def _report_custom_extensions(self, model: OSISemanticModel, issues: List[ConverterIssue]) -> None: + def _report_custom_extensions(self, model: OssieSemanticModel, issues: List[ConverterIssue]) -> None: elements = [(model.name, model.custom_extensions)] for dataset in model.datasets: elements.append((dataset.name, dataset.custom_extensions)) @@ -147,18 +147,18 @@ def _report_custom_extensions(self, model: OSISemanticModel, issues: List[Conver ConverterIssue(issue_type=ConverterIssueType.CUSTOM_EXTENSION_DROPPED, element_name=name) ) - def _infer_dataset_dialect(self, dataset: OSIDataset) -> OSIDialect: + def _infer_dataset_dialect(self, dataset: OssieDataset) -> OssieDialect: for field in dataset.fields or []: for entry in field.expression.dialects: - if entry.dialect in (OSIDialect.SNOWFLAKE, OSIDialect.DATABRICKS, OSIDialect.BIGQUERY): + if entry.dialect in (OssieDialect.SNOWFLAKE, OssieDialect.DATABRICKS, OssieDialect.BIGQUERY): return entry.dialect - return OSIDialect.ANSI_SQL + return OssieDialect.ANSI_SQL - def _split_ai_context(self, model: OSISemanticModel, issues: List[ConverterIssue]) -> Tuple[str, List[dict]]: + def _split_ai_context(self, model: OssieSemanticModel, issues: List[ConverterIssue]) -> Tuple[str, List[dict]]: ai_context = model.ai_context if ai_context is None: return "", [] - if isinstance(ai_context, OSIAIContextObject): + if isinstance(ai_context, OssieAIContextObject): if ai_context.synonyms or ai_context.examples: issues.append(ConverterIssue(issue_type=ConverterIssueType.AI_CONTEXT_DROPPED, element_name=model.name)) text = ai_context.instructions or "" @@ -189,9 +189,9 @@ def _split_ai_context(self, model: OSISemanticModel, issues: List[ConverterIssue def _convert_dataset( self, - dataset: OSIDataset, + dataset: OssieDataset, zsheet_refs: Dict[str, dict], - dialect: OSIDialect, + dialect: OssieDialect, measures: List[dict], issues: List[ConverterIssue], ) -> dict: @@ -262,7 +262,7 @@ def _convert_dataset( return zsheet def _ai_context_text(self, ai_context, element_name: str, issues: List[ConverterIssue]) -> str: - if isinstance(ai_context, OSIAIContextObject): + if isinstance(ai_context, OssieAIContextObject): if ai_context.synonyms or ai_context.examples: issues.append( ConverterIssue(issue_type=ConverterIssueType.AI_CONTEXT_DROPPED, element_name=element_name) @@ -279,13 +279,13 @@ def _split_source(self, source: str) -> Tuple[str, str, str]: return "", "", source def _pick_expression( - self, expression: OSIExpression, dialect: OSIDialect, element_name: str, issues: List[ConverterIssue] + self, expression: OssieExpression, dialect: OssieDialect, element_name: str, issues: List[ConverterIssue] ) -> str: by_dialect = {entry.dialect: entry.expression for entry in expression.dialects} if dialect in by_dialect: return by_dialect[dialect] - if OSIDialect.ANSI_SQL in by_dialect: - return by_dialect[OSIDialect.ANSI_SQL] + if OssieDialect.ANSI_SQL in by_dialect: + return by_dialect[OssieDialect.ANSI_SQL] issues.append( ConverterIssue(issue_type=ConverterIssueType.MISSING_DIALECT_EXPRESSION, element_name=element_name) ) @@ -295,7 +295,7 @@ def _is_bare_column(self, expression: str, name: str) -> bool: return expression in (name, f'"{name}"', f"`{name}`") def _assign_metrics( - self, model: OSISemanticModel, dataset_dialects: Dict[str, OSIDialect], issues: List[ConverterIssue] + self, model: OssieSemanticModel, dataset_dialects: Dict[str, OssieDialect], issues: List[ConverterIssue] ) -> Dict[str, List[dict]]: measures: Dict[str, List[dict]] = {} dataset_names = [dataset.name for dataset in model.datasets] @@ -336,7 +336,7 @@ def _find_referenced_dataset(self, expression: str, dataset_names: List[str]) -> return best def _convert_relationships( - self, model: OSISemanticModel, zsheet_refs: Dict[str, dict], issues: List[ConverterIssue] + self, model: OssieSemanticModel, zsheet_refs: Dict[str, dict], issues: List[ConverterIssue] ) -> List[dict]: edges = [] for relationship in model.relationships or []: diff --git a/converters/wisdom/src/ossie_wisdom/wisdom_to_osi.py b/converters/wisdom/src/ossie_wisdom/wisdom_to_ossie.py similarity index 84% rename from converters/wisdom/src/ossie_wisdom/wisdom_to_osi.py rename to converters/wisdom/src/ossie_wisdom/wisdom_to_ossie.py index 6fc8129d..6ab3347e 100644 --- a/converters/wisdom/src/ossie_wisdom/wisdom_to_osi.py +++ b/converters/wisdom/src/ossie_wisdom/wisdom_to_ossie.py @@ -26,45 +26,45 @@ from typing import Dict, List, Optional, Set, Tuple from ossie import ( - OSIDataset, - OSIDialect, - OSIDialectExpression, - OSIDimension, - OSIDocument, - OSIExpression, - OSIField, - OSIMetric, - OSIRelationship, - OSISemanticModel, + OssieDataset, + OssieDialect, + OssieDialectExpression, + OssieDimension, + OssieDocument, + OssieExpression, + OssieField, + OssieMetric, + OssieRelationship, + OssieSemanticModel, ) from ossie_wisdom.converter_issues import ConverterIssue, ConverterIssueType, ConverterResult -_DIALECT_MAP: Dict[str, OSIDialect] = { - "snowflake": OSIDialect.SNOWFLAKE, - "databricks": OSIDialect.DATABRICKS, - "bigquery": OSIDialect.BIGQUERY, - "ansi": OSIDialect.ANSI_SQL, - "ansi_sql": OSIDialect.ANSI_SQL, +_DIALECT_MAP: Dict[str, OssieDialect] = { + "snowflake": OssieDialect.SNOWFLAKE, + "databricks": OssieDialect.DATABRICKS, + "bigquery": OssieDialect.BIGQUERY, + "ansi": OssieDialect.ANSI_SQL, + "ansi_sql": OssieDialect.ANSI_SQL, } -_BACKTICK_DIALECTS = {OSIDialect.DATABRICKS, OSIDialect.BIGQUERY} +_BACKTICK_DIALECTS = {OssieDialect.DATABRICKS, OssieDialect.BIGQUERY} _TIME_DATA_TYPES = {"DATE", "DATETIME", "TIMESTAMP"} _SIMPLE_IDENTIFIER = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*$") -class WisdomToOSIConverter: +class WisdomToOssieConverter: """Converts a wisdom domain export dict into an Ossie Document.""" - def convert(self, export: dict) -> ConverterResult[OSIDocument]: + def convert(self, export: dict) -> ConverterResult[OssieDocument]: issues: List[ConverterIssue] = [] domain = export.get("domain", {}).get("zsheet_json", {}) dialect_by_connection = self._build_dialect_index(export, issues) datasets = [] - metrics: List[OSIMetric] = [] + metrics: List[OssieMetric] = [] for table in export.get("tables", []): zsheet = table.get("zsheet_json", {}) dialect = self._resolve_dialect(zsheet, dialect_by_connection) @@ -77,7 +77,7 @@ def convert(self, export: dict) -> ConverterResult[OSIDocument]: dataset_names = {d.name for d in datasets} relationships = self._convert_relationships(domain, dataset_names, issues) - model = OSISemanticModel( + model = OssieSemanticModel( name=domain.get("ref", {}).get("name") or export.get("export_metadata", {}).get("domain_name", "domain"), description=domain.get("description") or None, ai_context=self._build_ai_context(domain), @@ -85,15 +85,15 @@ def convert(self, export: dict) -> ConverterResult[OSIDocument]: relationships=relationships or None, metrics=[metric for _, metric in metrics] or None, ) - return ConverterResult(output=OSIDocument(semantic_model=[model]), issues=issues) + return ConverterResult(output=OssieDocument(semantic_model=[model]), issues=issues) - def _build_dialect_index(self, export: dict, issues: List[ConverterIssue]) -> Dict[str, OSIDialect]: - index: Dict[str, OSIDialect] = {} + def _build_dialect_index(self, export: dict, issues: List[ConverterIssue]) -> Dict[str, OssieDialect]: + index: Dict[str, OssieDialect] = {} for connection in export.get("connections", []): dialect_name = connection.get("dialect", "").lower() dialect = _DIALECT_MAP.get(dialect_name) if dialect is None: - dialect = OSIDialect.ANSI_SQL + dialect = OssieDialect.ANSI_SQL issues.append( ConverterIssue( issue_type=ConverterIssueType.UNSUPPORTED_DIALECT, @@ -103,9 +103,9 @@ def _build_dialect_index(self, export: dict, issues: List[ConverterIssue]) -> Di index[connection.get("connection_id", "")] = dialect return index - def _resolve_dialect(self, zsheet: dict, dialect_by_connection: Dict[str, OSIDialect]) -> OSIDialect: + def _resolve_dialect(self, zsheet: dict, dialect_by_connection: Dict[str, OssieDialect]) -> OssieDialect: connection_id = zsheet.get("location", {}).get("connectionId", "") - return dialect_by_connection.get(connection_id, OSIDialect.ANSI_SQL) + return dialect_by_connection.get(connection_id, OssieDialect.ANSI_SQL) def _build_ai_context(self, domain: dict) -> Optional[str]: parts: List[str] = [] @@ -118,14 +118,14 @@ def _build_ai_context(self, domain: dict) -> Optional[str]: parts.append(f"- {content}") return "\n".join(parts) or None - def _convert_table(self, zsheet: dict, dialect: OSIDialect, issues: List[ConverterIssue]) -> OSIDataset: + def _convert_table(self, zsheet: dict, dialect: OssieDialect, issues: List[ConverterIssue]) -> OssieDataset: name = zsheet.get("ref", {}).get("name", "") location = zsheet.get("location", {}) source = ".".join( part for part in (location.get("database"), location.get("schema"), location.get("dbTable")) if part ) - fields: List[OSIField] = [] + fields: List[OssieField] = [] seen: Set[str] = set() for column in zsheet.get("columns", []): field = self._convert_column(column, dialect) @@ -152,7 +152,7 @@ def _convert_table(self, zsheet: dict, dialect: OSIDialect, issues: List[Convert seen.add(field.name) fields.append(field) - return OSIDataset( + return OssieDataset( name=name, source=source, primary_key=self._extract_primary_key(zsheet), @@ -171,35 +171,35 @@ def _extract_primary_key(self, zsheet: dict) -> Optional[List[str]]: ] return flagged or None - def _convert_column(self, column: dict, dialect: OSIDialect) -> OSIField: + def _convert_column(self, column: dict, dialect: OssieDialect) -> OssieField: properties = column.get("properties", {}) - return OSIField( + return OssieField( name=column.get("name", ""), expression=self._make_expression(self._quote_identifier(column.get("name", ""), dialect), dialect), - dimension=OSIDimension(is_time=True) if properties.get("dataType") in _TIME_DATA_TYPES else None, + dimension=OssieDimension(is_time=True) if properties.get("dataType") in _TIME_DATA_TYPES else None, label=properties.get("displayName") or None, description=column.get("description") or None, ) - def _convert_formula(self, formula: dict, dialect: OSIDialect) -> Optional[OSIField]: + def _convert_formula(self, formula: dict, dialect: OssieDialect) -> Optional[OssieField]: name = formula.get("name", "") expression = formula.get("expression", "") if not name or not expression: return None properties = formula.get("properties", {}) - return OSIField( + return OssieField( name=name, expression=self._make_expression(expression, dialect), - dimension=OSIDimension(is_time=True) if properties.get("dataType") in _TIME_DATA_TYPES else None, + dimension=OssieDimension(is_time=True) if properties.get("dataType") in _TIME_DATA_TYPES else None, label=properties.get("displayName") or None, description=formula.get("description") or None, ) def _convert_measures( - self, zsheet: dict, dialect: OSIDialect, issues: List[ConverterIssue] - ) -> List[Tuple[str, OSIMetric]]: + self, zsheet: dict, dialect: OssieDialect, issues: List[ConverterIssue] + ) -> List[Tuple[str, OssieMetric]]: table_name = zsheet.get("ref", {}).get("name", "") - metrics: List[Tuple[str, OSIMetric]] = [] + metrics: List[Tuple[str, OssieMetric]] = [] for measure in zsheet.get("measures", []): name = measure.get("name", "") expression = measure.get("expression", "") @@ -212,7 +212,7 @@ def _convert_measures( metrics.append( ( table_name, - OSIMetric( + OssieMetric( name=name, expression=self._make_expression(expression, dialect), description=measure.get("description") or None, @@ -222,9 +222,9 @@ def _convert_measures( return metrics def _dedupe_metrics( - self, metrics: List[Tuple[str, OSIMetric]], issues: List[ConverterIssue] - ) -> List[Tuple[str, OSIMetric]]: - result: List[Tuple[str, OSIMetric]] = [] + self, metrics: List[Tuple[str, OssieMetric]], issues: List[ConverterIssue] + ) -> List[Tuple[str, OssieMetric]]: + result: List[Tuple[str, OssieMetric]] = [] seen: Set[str] = set() for table_name, metric in metrics: name = metric.name @@ -246,8 +246,8 @@ def _dedupe_metrics( def _convert_relationships( self, domain: dict, dataset_names: Set[str], issues: List[ConverterIssue] - ) -> List[OSIRelationship]: - relationships: List[OSIRelationship] = [] + ) -> List[OssieRelationship]: + relationships: List[OssieRelationship] = [] seen_names: Set[str] = set() edges = domain.get("relationshipGraph", {}).get("relationships", []) for edge in edges: @@ -300,7 +300,7 @@ def _convert_relationships( seen_names.add(name) relationships.append( - OSIRelationship( + OssieRelationship( name=name, from_dataset=from_dataset, to=to_dataset, @@ -349,7 +349,7 @@ def _flatten_compound_condition(self, compound: dict) -> Optional[List[Tuple[str return pairs or None return None - def _quote_identifier(self, name: str, dialect: OSIDialect) -> str: + def _quote_identifier(self, name: str, dialect: OssieDialect) -> str: """Quotes a column name for use as a SQL expression when it is not a plain identifier.""" if _SIMPLE_IDENTIFIER.match(name): return name @@ -357,5 +357,5 @@ def _quote_identifier(self, name: str, dialect: OSIDialect) -> str: return "`" + name.replace("`", "``") + "`" return '"' + name.replace('"', '""') + '"' - def _make_expression(self, expression: str, dialect: OSIDialect) -> OSIExpression: - return OSIExpression(dialects=[OSIDialectExpression(dialect=dialect, expression=expression)]) + def _make_expression(self, expression: str, dialect: OssieDialect) -> OssieExpression: + return OssieExpression(dialects=[OssieDialectExpression(dialect=dialect, expression=expression)]) diff --git a/converters/wisdom/tests/test_osi_to_wisdom.py b/converters/wisdom/tests/test_ossie_to_wisdom.py similarity index 77% rename from converters/wisdom/tests/test_osi_to_wisdom.py rename to converters/wisdom/tests/test_ossie_to_wisdom.py index 1ee17173..e74947bc 100644 --- a/converters/wisdom/tests/test_osi_to_wisdom.py +++ b/converters/wisdom/tests/test_ossie_to_wisdom.py @@ -21,33 +21,33 @@ import pytest from ossie import ( - OSIDataset, - OSIDialect, - OSIDialectExpression, - OSIDocument, - OSIExpression, - OSIField, - OSIRelationship, - OSISemanticModel, + OssieDataset, + OssieDialect, + OssieDialectExpression, + OssieDocument, + OssieExpression, + OssieField, + OssieRelationship, + OssieSemanticModel, ) -from ossie_wisdom import ConverterIssueType, OSIToWisdomConverter, WisdomToOSIConverter +from ossie_wisdom import ConverterIssueType, OssieToWisdomConverter, WisdomToOssieConverter FIXTURE = Path(__file__).parent / "fixtures" / "sample_export.json" def _snowflake(expression): - return OSIExpression(dialects=[OSIDialectExpression(dialect=OSIDialect.SNOWFLAKE, expression=expression)]) + return OssieExpression(dialects=[OssieDialectExpression(dialect=OssieDialect.SNOWFLAKE, expression=expression)]) @pytest.fixture(scope="module") -def osi_document(): +def ossie_document(): export = json.loads(FIXTURE.read_text()) - return WisdomToOSIConverter().convert(export).output + return WisdomToOssieConverter().convert(export).output @pytest.fixture(scope="module") -def result(osi_document): - return OSIToWisdomConverter().convert(osi_document, exported_at="2026-07-10T00:00:00+00:00") +def result(ossie_document): + return OssieToWisdomConverter().convert(ossie_document, exported_at="2026-07-10T00:00:00+00:00") @pytest.fixture(scope="module") @@ -132,33 +132,33 @@ def test_connections_are_per_dialect(export): assert _table(export, "tags")["location"]["connectionId"] == "et-connection-ansi" -def test_round_trip_preserves_osi_document(osi_document, export): - round_tripped = WisdomToOSIConverter().convert(export).output - assert round_tripped == osi_document +def test_round_trip_preserves_ossie_document(ossie_document, export): + round_tripped = WisdomToOssieConverter().convert(export).output + assert round_tripped == ossie_document -def test_deterministic_output(osi_document, export): - again = OSIToWisdomConverter().convert(osi_document, exported_at="2026-07-10T00:00:00+00:00").output +def test_deterministic_output(ossie_document, export): + again = OssieToWisdomConverter().convert(ossie_document, exported_at="2026-07-10T00:00:00+00:00").output assert again == export def test_extra_models_and_unrepresentable_elements_are_reported(): - dataset = OSIDataset( + dataset = OssieDataset( name="orders", source="analytics.sales.orders", unique_keys=[["order_id"]], fields=[ - OSIField(name="order_id", expression=_snowflake("order_id"), ai_context="the identifier"), + OssieField(name="order_id", expression=_snowflake("order_id"), ai_context="the identifier"), ], ) - second = OSISemanticModel(name="second", datasets=[OSIDataset(name="d", source="a.b.c")]) - document = OSIDocument( + second = OssieSemanticModel(name="second", datasets=[OssieDataset(name="d", source="a.b.c")]) + document = OssieDocument( semantic_model=[ - OSISemanticModel( + OssieSemanticModel( name="first", datasets=[dataset], relationships=[ - OSIRelationship( + OssieRelationship( name="orders_to_missing", from_dataset="orders", to="missing", @@ -170,7 +170,7 @@ def test_extra_models_and_unrepresentable_elements_are_reported(): second, ] ) - result = OSIToWisdomConverter().convert(document, exported_at="2026-07-10T00:00:00+00:00") + result = OssieToWisdomConverter().convert(document, exported_at="2026-07-10T00:00:00+00:00") assert [issue.element_name for issue in _issues_of(result, ConverterIssueType.EXTRA_MODEL_DROPPED)] == ["second"] assert [issue.element_name for issue in _issues_of(result, ConverterIssueType.UNIQUE_KEYS_DROPPED)] == ["orders"] assert [issue.element_name for issue in _issues_of(result, ConverterIssueType.AI_CONTEXT_DROPPED)] == [ @@ -183,16 +183,16 @@ def test_extra_models_and_unrepresentable_elements_are_reported(): def test_one_to_one_note_restores_relationship_type(): - document = OSIDocument( + document = OssieDocument( semantic_model=[ - OSISemanticModel( + OssieSemanticModel( name="m", datasets=[ - OSIDataset(name="a", source="db.s.a"), - OSIDataset(name="b", source="db.s.b"), + OssieDataset(name="a", source="db.s.a"), + OssieDataset(name="b", source="db.s.b"), ], relationships=[ - OSIRelationship( + OssieRelationship( name="a_to_b", from_dataset="a", to="b", @@ -204,24 +204,24 @@ def test_one_to_one_note_restores_relationship_type(): ) ] ) - export = OSIToWisdomConverter().convert(document, exported_at="2026-07-10T00:00:00+00:00").output + export = OssieToWisdomConverter().convert(document, exported_at="2026-07-10T00:00:00+00:00").output edges = export["domain"]["zsheet_json"]["relationshipGraph"]["relationships"] assert edges[0]["properties"]["relationshipType"] == "ONE_TO_ONE" def test_unresolved_metric_attaches_to_first_dataset(): - from ossie import OSIMetric + from ossie import OssieMetric - document = OSIDocument( + document = OssieDocument( semantic_model=[ - OSISemanticModel( + OssieSemanticModel( name="m", - datasets=[OSIDataset(name="a", source="db.s.a"), OSIDataset(name="b", source="db.s.b")], - metrics=[OSIMetric(name="row_count", expression=_snowflake("COUNT(*)"))], + datasets=[OssieDataset(name="a", source="db.s.a"), OssieDataset(name="b", source="db.s.b")], + metrics=[OssieMetric(name="row_count", expression=_snowflake("COUNT(*)"))], ) ] ) - result = OSIToWisdomConverter().convert(document, exported_at="2026-07-10T00:00:00+00:00") + result = OssieToWisdomConverter().convert(document, exported_at="2026-07-10T00:00:00+00:00") export = result.output assert [measure["name"] for measure in _table(export, "a")["measures"]] == ["row_count"] assert [issue.element_name for issue in _issues_of(result, ConverterIssueType.METRIC_TABLE_UNRESOLVED)] == [ diff --git a/converters/wisdom/tests/test_wisdom_to_osi.py b/converters/wisdom/tests/test_wisdom_to_ossie.py similarity index 90% rename from converters/wisdom/tests/test_wisdom_to_osi.py rename to converters/wisdom/tests/test_wisdom_to_ossie.py index eaa1c2dc..00839579 100644 --- a/converters/wisdom/tests/test_wisdom_to_osi.py +++ b/converters/wisdom/tests/test_wisdom_to_ossie.py @@ -21,8 +21,8 @@ import pytest import yaml -from ossie import OSIDialect, OSIDocument -from ossie_wisdom import ConverterIssueType, WisdomToOSIConverter +from ossie import OssieDialect, OssieDocument +from ossie_wisdom import ConverterIssueType, WisdomToOssieConverter FIXTURE = Path(__file__).parent / "fixtures" / "sample_export.json" @@ -30,7 +30,7 @@ @pytest.fixture(scope="module") def result(): export = json.loads(FIXTURE.read_text()) - return WisdomToOSIConverter().convert(export) + return WisdomToOssieConverter().convert(export) @pytest.fixture(scope="module") @@ -71,7 +71,7 @@ def test_columns_become_fields(model): orders = next(dataset for dataset in model.datasets if dataset.name == "orders") fields = {field.name: field for field in orders.fields} assert fields["order_id"].expression.dialects[0].expression == "order_id" - assert fields["order_id"].expression.dialects[0].dialect is OSIDialect.SNOWFLAKE + assert fields["order_id"].expression.dialects[0].dialect is OssieDialect.SNOWFLAKE assert fields["order_date"].dimension.is_time is True assert fields["order_id"].dimension is None assert fields["status"].label == "Order Status" @@ -87,7 +87,7 @@ def test_formulas_become_fields(model): fields = {field.name: field for field in orders.fields} is_large = fields["is_large"] assert is_large.expression.dialects[0].expression == 'CASE WHEN "orders"."amount" > 100 THEN TRUE ELSE FALSE END' - assert is_large.expression.dialects[0].dialect is OSIDialect.SNOWFLAKE + assert is_large.expression.dialects[0].dialect is OssieDialect.SNOWFLAKE assert is_large.label == "Is Large" assert is_large.description == "true when the order amount exceeds 100" @@ -101,7 +101,7 @@ def test_formula_colliding_with_column_is_dropped(result, model): def test_unsupported_dialect_falls_back_to_ansi(result, model): tags = next(dataset for dataset in model.datasets if dataset.name == "tags") - assert all(field.expression.dialects[0].dialect is OSIDialect.ANSI_SQL for field in tags.fields) + assert all(field.expression.dialects[0].dialect is OssieDialect.ANSI_SQL for field in tags.fields) unsupported = _issues_of(result, ConverterIssueType.UNSUPPORTED_DIALECT) assert len(unsupported) == 1 assert "postgres" in unsupported[0].element_name @@ -145,7 +145,7 @@ def test_measures_become_metrics(result, model): assert set(metrics) == {"total_amount", "customers_total_amount"} total = metrics["total_amount"] assert total.expression.dialects[0].expression == 'SUM("orders"."amount")' - assert total.expression.dialects[0].dialect is OSIDialect.SNOWFLAKE + assert total.expression.dialects[0].dialect is OssieDialect.SNOWFLAKE assert total.description == "Total order amount" collisions = _issues_of(result, ConverterIssueType.METRIC_NAME_COLLISION) assert [issue.element_name for issue in collisions] == ["customers.total_amount"] @@ -157,11 +157,11 @@ def test_stale_measure_is_kept_with_warning(result, model): assert [issue.element_name for issue in stale] == ["orders.total_amount"] -def test_output_round_trips_through_osi_yaml(result): - document = OSIDocument.model_validate(yaml.safe_load(result.output.to_osi_yaml())) +def test_output_round_trips_through_ossie_yaml(result): + document = OssieDocument.model_validate(yaml.safe_load(result.output.to_ossie_yaml())) assert document == result.output def test_export_without_tables_is_rejected(): with pytest.raises(ValueError, match="no tables"): - WisdomToOSIConverter().convert({"domain": {"zsheet_json": {"ref": {"name": "empty"}}}, "tables": []}) + WisdomToOssieConverter().convert({"domain": {"zsheet_json": {"ref": {"name": "empty"}}}, "tables": []}) diff --git a/core-spec/expression_language.md b/core-spec/expression_language.md index 42299977..db6b966b 100644 --- a/core-spec/expression_language.md +++ b/core-spec/expression_language.md @@ -105,7 +105,7 @@ Ossie expressions support the following SQL constructs within any expression: | Construct | Notes | | :---- |:--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| Column and Metric references | Varies based on whether in Ontology or Semantic models. See namespaceing in [OSI Discussion Point: Core Analytic Abstractions](https://docs.google.com/document/d/1si8DqU4arG18ZgX4HnRG5D_zS2X7V1s-vgNY35rvxhM/edit?tab=t.0#heading=h.le505t8uoyfy) And future Ontology documentation | +| Column and Metric references | Varies based on whether in Ontology or Semantic models. See namespaceing in [Ossie Discussion Point: Core Analytic Abstractions](https://docs.google.com/document/d/1si8DqU4arG18ZgX4HnRG5D_zS2X7V1s-vgNY35rvxhM/edit?tab=t.0#heading=h.le505t8uoyfy) And future Ontology documentation | | Arithmetic operators | `+`, `-`, `*`, `/`, `%` (modulo) | | Comparison operators | `=`, `<>`, `!=`, `<`, `>`, `<=`, `>=` | | Logical operators | `AND`, `OR`, `NOT` | diff --git a/core-spec/osi-schema.json b/core-spec/ossie-schema.json similarity index 99% rename from core-spec/osi-schema.json rename to core-spec/ossie-schema.json index f24e45f1..edddac31 100644 --- a/core-spec/osi-schema.json +++ b/core-spec/ossie-schema.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/apache/ossie/core-spec/osi-schema.json", + "$id": "https://github.com/apache/ossie/core-spec/ossie-schema.json", "title": "Apache Ossie Core Metadata Specification", "description": "JSON Schema for validating Apache Ossie semantic model definitions", "type": "object", diff --git a/core-spec/spec.yaml b/core-spec/spec.yaml index 32fbb3e1..9b21b444 100644 --- a/core-spec/spec.yaml +++ b/core-spec/spec.yaml @@ -40,7 +40,7 @@ dialects: # Supported logical data types for fields and metrics # TODO: Generate this list from the authoritative DataType enum in -# osi-schema.json ($defs.DataType.enum); until then, keep them in sync. +# ossie-schema.json ($defs.DataType.enum); until then, keep them in sync. datatypes: - "String" # Variable-length Unicode character data - "Integer" # Exact integral number diff --git a/docs/index.md b/docs/index.md index 738c5724..3836092d 100644 --- a/docs/index.md +++ b/docs/index.md @@ -188,7 +188,7 @@ A typical Ossie-based workflow looks like this: 1. **Author**: A semantic model is authored in one tool (e.g., dbt) or directly in the Ossie YAML format. 2. **Import**: If authored in a vendor tool, the vendor's import converter translates it into an Ossie model, preserving vendor-specific metadata in `custom_extensions`. -3. **Validate**: The Ossie model is validated against the [JSON Schema](../core-spec/osi-schema.json) and the [validation script](../validation/validate.py) to ensure correctness. +3. **Validate**: The Ossie model is validated against the [JSON Schema](../core-spec/ossie-schema.json) and the [validation script](../validation/validate.py) to ensure correctness. 4. **Exchange**: The Ossie model is shared — via Git, a data catalog, or a sync API — with other teams and tools. 5. **Export**: Each consuming tool's export converter translates the Ossie model into its native format, selecting the appropriate SQL dialect and applying vendor-specific extensions. 6. **Round-Trip**: When changes are made in a downstream tool, they can be imported back into the Ossie model, preserving all metadata for lossless round-tripping. @@ -237,7 +237,7 @@ You can contribute a converter. The [Converters Guide](../converters/README.md) No. Import converters translate existing vendor-specific models into the Ossie format automatically. Your existing models remain intact — Ossie provides an additional interchange layer on top of them. **How do I validate an Ossie model?** -Use the [validation script](../validation/validate.py) included in the repository. It checks your model against the [JSON Schema](../core-spec/osi-schema.json), validates SQL expressions across dialects, and ensures referential integrity between datasets and relationships. +Use the [validation script](../validation/validate.py) included in the repository. It checks your model against the [JSON Schema](../core-spec/ossie-schema.json), validates SQL expressions across dialects, and ensures referential integrity between datasets and relationships. ### Technical @@ -338,7 +338,7 @@ A practical guide for organizations looking to adopt Ossie. - **GitHub**: [github.com/apache/ossie](https://github.com/apache/ossie) - **Slack**: [join slack](https://join.slack.com/t/opensemanticx/shared_invite/zt-3yuad6c0h-MaoPgVSD1g9MEOf1_QeaiQ) - **Core Specification**: [core-spec/spec.md](../core-spec/spec.md) -- **JSON Schema**: [core-spec/osi-schema.json](../core-spec/osi-schema.json) +- **JSON Schema**: [core-spec/ossie-schema.json](../core-spec/ossie-schema.json) - **YAML Schema**: [core-spec/spec.yaml](../core-spec/spec.yaml) - **TPC-DS Example Model**: [examples/tpcds_semantic_model.yaml](../examples/tpcds_semantic_model.yaml) - **Validation Script**: [validation/validate.py](../validation/validate.py) diff --git a/examples/tpcds_semantic_model.yaml b/examples/tpcds_semantic_model.yaml index a04b1683..f192c189 100644 --- a/examples/tpcds_semantic_model.yaml +++ b/examples/tpcds_semantic_model.yaml @@ -1,4 +1,4 @@ -# yaml-language-server: $schema=../core-spec/osi-schema.json +# yaml-language-server: $schema=../core-spec/ossie-schema.json # 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 diff --git a/ontology/ontology.json b/ontology/ontology.json index 81578a39..22c889e7 100644 --- a/ontology/ontology.json +++ b/ontology/ontology.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/apache/ossie/core-spec/osi-schema.json", + "$id": "https://github.com/apache/ossie/core-spec/ossie-schema.json", "title": "Apache Ossie Ontology Metadata Specification", "description": "JSON Schema for validating Apache Ossie ontology definitions", "type": "object", @@ -19,7 +19,7 @@ "description": "Human-readable description" }, "ai_context": { - "$ref": "https://raw.githubusercontent.com/apache/ossie/main/core-spec/osi-schema.json#/$defs/AIContext" + "$ref": "https://raw.githubusercontent.com/apache/ossie/main/core-spec/ossie-schema.json#/$defs/AIContext" }, "requires": { "type": "array", @@ -281,7 +281,7 @@ "description": "Human-readable description of this ontology map" }, "semantic_model": { - "$ref": "https://raw.githubusercontent.com/apache/ossie/main/core-spec/osi-schema.json#/$defs/SemanticModel" + "$ref": "https://raw.githubusercontent.com/apache/ossie/main/core-spec/ossie-schema.json#/$defs/SemanticModel" }, "concept_mappings": { "type": "array", diff --git a/python/README.md b/python/README.md index 38af97d1..dc8d67bc 100644 --- a/python/README.md +++ b/python/README.md @@ -19,7 +19,7 @@ # Apache Ossie Python Package -The Apache Ossie Python package provides Pydantic v2 models for the Apache Ossie semantic model specification. It is the shared fundation used by Apache Ossie converters to parse, construct, validate, and serialize OSI documents from Python application. +The Apache Ossie Python package provides Pydantic v2 models for the Apache Ossie semantic model specification. It is the shared fundation used by Apache Ossie converters to parse, construct, validate, and serialize Ossie documents from Python application. ## Development diff --git a/python/src/ossie/__init__.py b/python/src/ossie/__init__.py index c7b33604..d01b36e9 100644 --- a/python/src/ossie/__init__.py +++ b/python/src/ossie/__init__.py @@ -16,37 +16,37 @@ # under the License. from ossie.models import ( - OSIAIContext, - OSIAIContextObject, - OSICustomExtension, - OSIDataset, - OSIDataType, - OSIDialect, - OSIDialectExpression, - OSIDimension, - OSIDocument, - OSIExpression, - OSIField, - OSIMetric, - OSIRelationship, - OSISemanticModel, - OSIVendor, + OssieAIContext, + OssieAIContextObject, + OssieCustomExtension, + OssieDataset, + OssieDataType, + OssieDialect, + OssieDialectExpression, + OssieDimension, + OssieDocument, + OssieExpression, + OssieField, + OssieMetric, + OssieRelationship, + OssieSemanticModel, + OssieVendor, ) __all__ = [ - "OSIAIContext", - "OSIAIContextObject", - "OSICustomExtension", - "OSIDataset", - "OSIDataType", - "OSIDialect", - "OSIDialectExpression", - "OSIDimension", - "OSIDocument", - "OSIExpression", - "OSIField", - "OSIMetric", - "OSIRelationship", - "OSISemanticModel", - "OSIVendor", + "OssieAIContext", + "OssieAIContextObject", + "OssieCustomExtension", + "OssieDataset", + "OssieDataType", + "OssieDialect", + "OssieDialectExpression", + "OssieDimension", + "OssieDocument", + "OssieExpression", + "OssieField", + "OssieMetric", + "OssieRelationship", + "OssieSemanticModel", + "OssieVendor", ] diff --git a/python/src/ossie/models.py b/python/src/ossie/models.py index 5406a743..7ea515c3 100644 --- a/python/src/ossie/models.py +++ b/python/src/ossie/models.py @@ -22,7 +22,7 @@ from pydantic import BaseModel, ConfigDict, Field -class OSIDialect(str, Enum): +class OssieDialect(str, Enum): """Supported SQL and expression language dialects.""" ANSI_SQL = "ANSI_SQL" @@ -34,7 +34,7 @@ class OSIDialect(str, Enum): BIGQUERY = "BIGQUERY" -class OSIDataType(str, Enum): +class OssieDataType(str, Enum): """Portable logical data types for fields and metric results.""" STRING = "String" @@ -51,15 +51,15 @@ class OSIDataType(str, Enum): _TEMPORAL_DATA_TYPES = frozenset( { - OSIDataType.DATE, - OSIDataType.TIME, - OSIDataType.DATE_TIME, - OSIDataType.DATE_TIME_TZ, + OssieDataType.DATE, + OssieDataType.TIME, + OssieDataType.DATE_TIME, + OssieDataType.DATE_TIME_TZ, } ) -class OSIVendor(str, Enum): +class OssieVendor(str, Enum): """Well-known vendor names for custom extensions.""" COMMON = "COMMON" @@ -72,7 +72,7 @@ class OSIVendor(str, Enum): WISDOM = "WISDOM" -class OSIAIContextObject(BaseModel): +class OssieAIContextObject(BaseModel): """Structured AI context with instructions, synonyms, and examples.""" model_config = ConfigDict(frozen=True, extra="allow") @@ -82,10 +82,10 @@ class OSIAIContextObject(BaseModel): examples: Optional[tuple[str, ...]] = None -OSIAIContext = Union[str, OSIAIContextObject] +OssieAIContext = Union[str, OssieAIContextObject] -class OSICustomExtension(BaseModel): +class OssieCustomExtension(BaseModel): """Vendor-specific metadata as a serialized JSON string.""" model_config = ConfigDict(frozen=True) @@ -94,24 +94,24 @@ class OSICustomExtension(BaseModel): data: str -class OSIDialectExpression(BaseModel): +class OssieDialectExpression(BaseModel): """Expression in a specific dialect.""" model_config = ConfigDict(frozen=True) - dialect: OSIDialect + dialect: OssieDialect expression: str -class OSIExpression(BaseModel): +class OssieExpression(BaseModel): """Expression definition with multi-dialect support.""" model_config = ConfigDict(frozen=True) - dialects: list[OSIDialectExpression] + dialects: list[OssieDialectExpression] -class OSIDimension(BaseModel): +class OssieDimension(BaseModel): """Dimension metadata on a field.""" model_config = ConfigDict(frozen=True) @@ -119,19 +119,19 @@ class OSIDimension(BaseModel): is_time: Optional[bool] = None -class OSIField(BaseModel): +class OssieField(BaseModel): """Row-level attribute for grouping, filtering, and metric expressions.""" model_config = ConfigDict(frozen=True) name: str - expression: OSIExpression - dimension: Optional[OSIDimension] = None + expression: OssieExpression + dimension: Optional[OssieDimension] = None label: Optional[str] = None description: Optional[str] = None - datatype: Optional[OSIDataType] = None - ai_context: Optional[OSIAIContext] = None - custom_extensions: Optional[list[OSICustomExtension]] = None + datatype: Optional[OssieDataType] = None + ai_context: Optional[OssieAIContext] = None + custom_extensions: Optional[list[OssieCustomExtension]] = None def is_time_dimension(self) -> bool: """Return the field's effective temporal-dimension role. @@ -147,7 +147,7 @@ def is_time_dimension(self) -> bool: return self.datatype in _TEMPORAL_DATA_TYPES -class OSIDataset(BaseModel): +class OssieDataset(BaseModel): """Logical dataset representing a business entity (fact or dimension table).""" model_config = ConfigDict(frozen=True) @@ -157,12 +157,12 @@ class OSIDataset(BaseModel): primary_key: Optional[list[str]] = None unique_keys: Optional[list[list[str]]] = None description: Optional[str] = None - ai_context: Optional[OSIAIContext] = None - fields: Optional[list[OSIField]] = None - custom_extensions: Optional[list[OSICustomExtension]] = None + ai_context: Optional[OssieAIContext] = None + fields: Optional[list[OssieField]] = None + custom_extensions: Optional[list[OssieCustomExtension]] = None -class OSIRelationship(BaseModel): +class OssieRelationship(BaseModel): """Foreign key relationship between datasets.""" model_config = ConfigDict(frozen=True, populate_by_name=True) @@ -172,52 +172,52 @@ class OSIRelationship(BaseModel): to: str from_columns: list[str] to_columns: list[str] - ai_context: Optional[OSIAIContext] = None - custom_extensions: Optional[list[OSICustomExtension]] = None + ai_context: Optional[OssieAIContext] = None + custom_extensions: Optional[list[OssieCustomExtension]] = None -class OSIMetric(BaseModel): +class OssieMetric(BaseModel): """Quantitative measure defined on business data.""" model_config = ConfigDict(frozen=True) name: str - expression: OSIExpression + expression: OssieExpression description: Optional[str] = None - datatype: Optional[OSIDataType] = None - ai_context: Optional[OSIAIContext] = None - custom_extensions: Optional[list[OSICustomExtension]] = None + datatype: Optional[OssieDataType] = None + ai_context: Optional[OssieAIContext] = None + custom_extensions: Optional[list[OssieCustomExtension]] = None -class OSISemanticModel(BaseModel): +class OssieSemanticModel(BaseModel): """Top-level container representing a complete semantic model.""" model_config = ConfigDict(frozen=True) name: str description: Optional[str] = None - ai_context: Optional[OSIAIContext] = None - datasets: list[OSIDataset] - relationships: Optional[list[OSIRelationship]] = None - metrics: Optional[list[OSIMetric]] = None - custom_extensions: Optional[list[OSICustomExtension]] = None + ai_context: Optional[OssieAIContext] = None + datasets: list[OssieDataset] + relationships: Optional[list[OssieRelationship]] = None + metrics: Optional[list[OssieMetric]] = None + custom_extensions: Optional[list[OssieCustomExtension]] = None -class OSIDocument(BaseModel): +class OssieDocument(BaseModel): """Root Ossie document.""" model_config = ConfigDict(frozen=True) version: str = "0.2.0.dev0" - dialects: Optional[list[OSIDialect]] = None - vendors: Optional[list[OSIVendor]] = None - semantic_model: list[OSISemanticModel] + dialects: Optional[list[OssieDialect]] = None + vendors: Optional[list[OssieVendor]] = None + semantic_model: list[OssieSemanticModel] - def to_osi_yaml(self, **kwargs: Any) -> str: + def to_ossie_yaml(self, **kwargs: Any) -> str: """Serialize to Ossie-compliant YAML (uses field aliases and excludes None values).""" data = self.model_dump(by_alias=True, exclude_none=True, mode="json", **kwargs) return yaml.dump(data, default_flow_style=False, sort_keys=False, allow_unicode=True) - def to_osi_json(self, **kwargs: Any) -> str: + def to_ossie_json(self, **kwargs: Any) -> str: """Serialize to Ossie-compliant JSON (uses field aliases and excludes None values).""" return self.model_dump_json(by_alias=True, exclude_none=True, **kwargs) diff --git a/python/tests/test_models.py b/python/tests/test_models.py index 749a226c..74d2a320 100644 --- a/python/tests/test_models.py +++ b/python/tests/test_models.py @@ -23,11 +23,11 @@ from pydantic import ValidationError from ossie import ( - OSIDataType, - OSIDimension, - OSIDocument, - OSIExpression, - OSIField, + OssieDataType, + OssieDimension, + OssieDocument, + OssieExpression, + OssieField, ) @@ -35,8 +35,8 @@ def _expression_data(value: str = "value") -> dict: return {"dialects": [{"dialect": "ANSI_SQL", "expression": value}]} -def _expression(value: str = "value") -> OSIExpression: - return OSIExpression.model_validate(_expression_data(value)) +def _expression(value: str = "value") -> OssieExpression: + return OssieExpression.model_validate(_expression_data(value)) def _document() -> dict: @@ -72,10 +72,10 @@ def _document() -> dict: def test_data_type_enum_matches_core_schema() -> None: - schema_path = Path(__file__).parents[2] / "core-spec" / "osi-schema.json" + schema_path = Path(__file__).parents[2] / "core-spec" / "ossie-schema.json" schema = json.loads(schema_path.read_text()) - assert [member.value for member in OSIDataType] == schema["$defs"]["DataType"][ + assert [member.value for member in OssieDataType] == schema["$defs"]["DataType"][ "enum" ] assert schema["$defs"]["Field"]["properties"]["datatype"] == { @@ -87,15 +87,15 @@ def test_data_type_enum_matches_core_schema() -> None: def test_field_and_metric_datatypes_survive_serialization() -> None: - document = OSIDocument.model_validate(_document()) + document = OssieDocument.model_validate(_document()) field = document.semantic_model[0].datasets[0].fields[0] metric = document.semantic_model[0].metrics[0] - assert field.datatype is OSIDataType.DATE_TIME_TZ - assert metric.datatype is OSIDataType.DECIMAL + assert field.datatype is OssieDataType.DATE_TIME_TZ + assert metric.datatype is OssieDataType.DECIMAL - as_json = json.loads(document.to_osi_json()) - as_yaml = yaml.safe_load(document.to_osi_yaml()) + as_json = json.loads(document.to_ossie_json()) + as_yaml = yaml.safe_load(document.to_ossie_yaml()) for serialized in (as_json, as_yaml): model = serialized["semantic_model"][0] assert model["datasets"][0]["fields"][0]["datatype"] == "DateTimeTz" @@ -108,26 +108,26 @@ def test_invalid_datatype_is_rejected() -> None: field["datatype"] = "timestamp" with pytest.raises(ValidationError): - OSIDocument.model_validate(document) + OssieDocument.model_validate(document) @pytest.mark.parametrize( ("dimension", "datatype", "expected"), [ - (None, OSIDataType.DATE, False), - (OSIDimension(), OSIDataType.DATE, True), - (OSIDimension(is_time=False), OSIDataType.DATE_TIME_TZ, False), - (OSIDimension(is_time=True), OSIDataType.STRING, True), - (OSIDimension(), OSIDataType.STRING, False), - (OSIDimension(), None, False), + (None, OssieDataType.DATE, False), + (OssieDimension(), OssieDataType.DATE, True), + (OssieDimension(is_time=False), OssieDataType.DATE_TIME_TZ, False), + (OssieDimension(is_time=True), OssieDataType.STRING, True), + (OssieDimension(), OssieDataType.STRING, False), + (OssieDimension(), None, False), ], ) def test_effective_time_dimension_role( - dimension: OSIDimension | None, - datatype: OSIDataType | None, + dimension: OssieDimension | None, + datatype: OssieDataType | None, expected: bool, ) -> None: - field = OSIField( + field = OssieField( name="value", expression=_expression(), dimension=dimension, diff --git a/validation/validate.py b/validation/validate.py index 258d34f1..d7a12637 100644 --- a/validation/validate.py +++ b/validation/validate.py @@ -227,7 +227,7 @@ def main(): args = sys.argv[1:] yaml_path = Path(args[0]) - schema_path = Path(__file__).parent.parent / "core-spec" / "osi-schema.json" + schema_path = Path(__file__).parent.parent / "core-spec" / "ossie-schema.json" if len(args) > 1: if len(args) == 3 and args[1] == "--schema": schema_path = Path(args[2])