Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 86 additions & 0 deletions converters/semantido/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
-->

# semantido ⇄ Apache Ossie converter

Converts between [semantido](https://github.com/hikarilabs/semantido) — a
code-native semantic layer authored as decorators on SQLAlchemy models and
Apache Ossie semantic model documents.

Unlike file-to-file converters, semantido's source of truth is live Python:
the forward direction imports a module of decorated models, syncs the
semantic layer, and emits Ossie YAML through the typed `apache-ossie`
objects. The reverse direction is code generation: it produces a Python
module of `@semantic_table`-decorated models from an Ossie document.

## Usage

```bash
# decorated SQLAlchemy models -> Ossie YAML
ossie-semantido semantido-to-osi \
-m models.[SQLAlchemy model] -p ./my_project \
-n [model name] -o [model].osi.yaml

# Ossie YAML -> generated semantido model code
ossie-semantido osi-to-semantido \
-i [model].osi.yaml -o generated_model_[model name].py
```

## Mapping

| semantido | Ossie | Notes |
|-------------------------------------------|-----------------------------------|------------------------------------------------------|
| decorated table | `datasets[]` | `source` from schema-qualified table name |
| column | `fields[]` | expression emitted as `ANSI_SQL` column reference |
| `description` / `<col>_description` | `description` | |
| `business_context`, `application_context` | dataset `ai_context.instructions` | |
| `synonyms` / `<col>_synonyms` | `ai_context.synonyms` | |
| `<col>_sample_values` | field `ai_context.examples` | |
| `time_dimension=` + `<col>_time_grain` | field `dimension.is_time` | grain preserved in extensions |
| FK relationships | `relationships[]` | join condition parsed to `from_columns`/`to_columns` |
| `sql_filters` | dataset `custom_extensions` | no Ossie core field (see below) |
| `<col>_privacy_level` | field `custom_extensions` | no Ossie core field (see below) |

### Metadata carried in `custom_extensions`

semantido captures runtime-governance metadata that has no core-spec home
in Ossie today: per-field privacy classification, default SQL filters /
row-level security fragments, and time-dimension grain. These are preserved
losslessly under `vendor_name: SEMANTIDO` with `data` as a serialized JSON
string, so no information is lost in interchange — but tools without
semantido awareness will not interpret them.

### Known lossy conversions

Conversions report structured `ConverterIssue`s (mirroring `ossie_dbt`):
Ossie metrics and non-ANSI dialect expressions have no semantido
equivalent and are dropped with a recorded issue; generated code lists
them in a TODO block for review.

## Tests

The test fixture is an EMIR (European Market Infrastructure Regulation)
trade-reporting model — a regulated-industry schema whose semantics
(role-bridge fan-out, signed vs. unsigned amount conventions,
notional/valuation ambiguity) demonstrate why interchange must carry
business meaning, not just structure.

```bash
uv run pytest tests/
```
54 changes: 54 additions & 0 deletions converters/semantido/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[project]
name = "apache-ossie-semantido"
version = "0.1.0.dev0"
description = "semantido (SQLAlchemy code-native semantic layer) <> Apache Ossie converter"
requires-python = ">=3.11"
classifiers = [
"License :: OSI Approved :: Apache Software License",
"Programming Language :: Python :: 3",
]
dependencies = [
"apache-ossie>=0.2.0.dev0",
"semantido>=0.4.1",
"SQLAlchemy>=2.0",
"PyYAML>=6.0",
]

[project.license]
text = "Apache-2.0"

[project.scripts]
ossie-semantido = "ossie_semantido.cli:main"

[tool.hatch.build.targets.wheel]
packages = ["src/ossie_semantido"]

[tool.uv]
dev-dependencies = [
"pytest>=8.0",
"ruff==0.15.22"
]

[tool.uv.sources]
apache-ossie = { path = "../../python", editable = true }
36 changes: 36 additions & 0 deletions converters/semantido/src/ossie_semantido/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.

"""semantido <> Apache Ossie converter."""

from ossie_semantido.converter_issues import (
ConverterIssue,
ConverterIssueType,
ConverterResult,
)
from ossie_semantido.loaders import load_from_module
from ossie_semantido.osi_to_semantido import osi_to_semantido_source
from ossie_semantido.semantido_to_osi import semantic_layer_to_osi

__all__ = [
"ConverterIssue",
"ConverterIssueType",
"ConverterResult",
"load_from_module",
"osi_to_semantido_source",
"semantic_layer_to_osi",
]
115 changes: 115 additions & 0 deletions converters/semantido/src/ossie_semantido/cli.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.

"""Command-line interface for the semantido <> Apache Ossie converter."""

import argparse
import sys

import yaml
from ossie import OSIDocument

from ossie_semantido.loaders import load_from_module
from ossie_semantido.osi_to_semantido import osi_to_semantido_source
from ossie_semantido.semantido_to_osi import semantic_layer_to_osi


def _report_issues(issues) -> None:
for issue in issues:
print(
f"warning: {issue.issue_type.value}: {issue.element_name}", file=sys.stderr
)


def _cmd_semantido_to_osi(args: argparse.Namespace) -> None:
layer = load_from_module(args.module, sys_path=args.path)
result = semantic_layer_to_osi(layer, model_name=args.name)
_report_issues(result.issues)
with open(args.output, "w", encoding="utf-8") as handle:
handle.write(result.output.to_osi_yaml())


def _cmd_osi_to_semantido(args: argparse.Namespace) -> None:
with open(args.input, "r", encoding="utf-8") as handle:
document = OSIDocument.model_validate(yaml.safe_load(handle))
result = osi_to_semantido_source(document)
_report_issues(result.issues)
with open(args.output, "w", encoding="utf-8") as handle:
handle.write(result.output)


def main() -> None:
parser = argparse.ArgumentParser(
prog="ossie-semantido",
description="Convert between semantido semantic layers and Apache Ossie documents",
)
subparsers = parser.add_subparsers(dest="command", required=True)

to_osi = subparsers.add_parser(
"semantido-to-osi", help="Convert decorated SQLAlchemy models → Ossie YAML"
)
to_osi.add_argument(
"-m",
"--module",
required=True,
metavar="MODULE",
help="Dotted module path of the semantido models, e.g. models.emir_reporting",
)
to_osi.add_argument(
"-p",
"--path",
default=None,
metavar="DIR",
help="Directory prepended to sys.path before import",
)
to_osi.add_argument(
"-n",
"--name",
required=True,
metavar="NAME",
help="Name for the Ossie semantic_model",
)
to_osi.add_argument(
"-o",
"--output",
required=True,
metavar="FILE",
help="Path for output Ossie YAML",
)
to_osi.set_defaults(func=_cmd_semantido_to_osi)

from_osi = subparsers.add_parser(
"osi-to-semantido", help="Convert Ossie YAML → generated semantido model code"
)
from_osi.add_argument(
"-i", "--input", required=True, metavar="FILE", help="Path to Ossie YAML"
)
from_osi.add_argument(
"-o",
"--output",
required=True,
metavar="FILE",
help="Path for generated Python module",
)
from_osi.set_defaults(func=_cmd_osi_to_semantido)

args = parser.parse_args()
args.func(args)


if __name__ == "__main__":
main()
20 changes: 20 additions & 0 deletions converters/semantido/src/ossie_semantido/constants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.

"""Shared constants for the ossie-semantido converter."""

VENDOR_NAME = "SEMANTIDO"
51 changes: 51 additions & 0 deletions converters/semantido/src/ossie_semantido/converter_issues.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.

"""Issue reporting for lossy conversions, mirroring the ossie_dbt pattern."""

from dataclasses import dataclass
from enum import Enum
from typing import Generic, List, TypeVar


class ConverterIssueType(Enum):
"""Identifies the kind of information loss that occurred during conversion."""

RELATIONSHIP_JOIN_UNPARSED = "RELATIONSHIP_JOIN_UNPARSED"
METRIC_DROPPED = "METRIC_DROPPED"
NON_ANSI_EXPRESSION_DROPPED = "NON_ANSI_EXPRESSION_DROPPED"
COMPOSITE_PRIMARY_KEY_TRUNCATED = "COMPOSITE_PRIMARY_KEY_TRUNCATED"
UNMAPPED_DATA_TYPE = "UNMAPPED_DATA_TYPE"


@dataclass(frozen=True)
class ConverterIssue:
"""Records a single instance of information loss during conversion."""

issue_type: ConverterIssueType
element_name: str


T = TypeVar("T")


@dataclass(frozen=True)
class ConverterResult(Generic[T]):
"""Return value of a converter's convert() method, pairing the output with any conversion issues."""

output: T
issues: List[ConverterIssue]
Loading