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

name: Python Models CI

on:
push:
branches: [ "main" ]
paths:
- 'python/**'
- '.github/workflows/python-ci.yml'
pull_request:
branches: [ "main" ]
paths:
- 'python/**'
- '.github/workflows/python-ci.yml'

jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.11", "3.12", "3.13", "3.14"]

steps:
- name: Checkout project
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: ${{ matrix.python-version }}

- name: Install uv
run: |
curl -LsSf https://astral.sh/uv/install.sh | sh
echo "${HOME}/.local/bin" >> "${GITHUB_PATH}"

- name: Sync dependencies
working-directory: python
run: |
uv sync

- name: Unit Tests
working-directory: python
run: |
uv run pytest
8 changes: 8 additions & 0 deletions python/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@
requires = ["hatchling"]
build-backend = "hatchling.build"

[dependency-groups]
dev = [
"pytest>=8.0",
]

[project]
name = "apache-ossie"
version = "0.2.0.dev0"
Expand All @@ -45,3 +50,6 @@ packages = ["src/ossie"]

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

import pytest

from ossie import OSIDialect, OSIExpression


def _expression_data(value: str = "value") -> dict:
return {"dialects": [{"dialect": OSIDialect.ANSI_SQL, "expression": value}]}


def _expression(value: str = "value") -> OSIExpression:
return OSIExpression.model_validate(_expression_data(value))


@pytest.fixture
def document_data() -> dict:
return {
"version": "0.2.0.dev0",
"semantic_model": [
{
"name": "typed_model",
"datasets": [
{
"name": "events",
"source": "catalog.schema.events",
"fields": [
{
"name": "occurred_at",
"expression": _expression_data("occurred_at"),
"dimension": {},
"datatype": "DateTimeTz",
}
],
}
],
"metrics": [
{
"name": "revenue",
"expression": _expression_data("SUM(events.revenue)"),
"datatype": "Decimal",
}
],
}
],
}
106 changes: 61 additions & 45 deletions python/tests/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,53 +22,21 @@
import yaml
from pydantic import ValidationError

from conftest import _expression

from ossie import (
OSIAIContextObject,
OSIDataType,
OSIDimension,
OSIDocument,
OSIExpression,
OSIField,
OSIRelationship,
)


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 _document() -> dict:
return {
"version": "0.2.0.dev0",
"semantic_model": [
{
"name": "typed_model",
"datasets": [
{
"name": "events",
"source": "catalog.schema.events",
"fields": [
{
"name": "occurred_at",
"expression": _expression_data("occurred_at"),
"dimension": {},
"datatype": "DateTimeTz",
}
],
}
],
"metrics": [
{
"name": "revenue",
"expression": _expression_data("SUM(events.revenue)"),
"datatype": "Decimal",
}
],
}
],
}
# ---------------------------------------------------------------------------
# Data type tests
# ---------------------------------------------------------------------------


def test_data_type_enum_matches_core_schema() -> None:
Expand All @@ -86,8 +54,8 @@ def test_data_type_enum_matches_core_schema() -> None:
}


def test_field_and_metric_datatypes_survive_serialization() -> None:
document = OSIDocument.model_validate(_document())
def test_field_and_metric_datatypes_survive_serialization(document_data: dict) -> None:
document = OSIDocument.model_validate(document_data)

field = document.semantic_model[0].datasets[0].fields[0]
metric = document.semantic_model[0].metrics[0]
Expand All @@ -102,13 +70,12 @@ def test_field_and_metric_datatypes_survive_serialization() -> None:
assert model["metrics"][0]["datatype"] == "Decimal"


def test_invalid_datatype_is_rejected() -> None:
document = _document()
field = document["semantic_model"][0]["datasets"][0]["fields"][0]
def test_invalid_datatype_is_rejected(document_data: dict) -> None:
field = document_data["semantic_model"][0]["datasets"][0]["fields"][0]
field["datatype"] = "timestamp"

with pytest.raises(ValidationError):
OSIDocument.model_validate(document)
OSIDocument.model_validate(document_data)


@pytest.mark.parametrize(
Expand All @@ -135,3 +102,52 @@ def test_effective_time_dimension_role(
)

assert field.is_time_dimension() is expected


# ---------------------------------------------------------------------------
# Model behavior
# ---------------------------------------------------------------------------


def test_ai_context_object_allows_extra() -> None:
ai_ctx = OSIAIContextObject(custom_field="custom_value")
assert ai_ctx.custom_field == "custom_value"


def test_ai_context_accepts_string(document_data: dict) -> None:
document_data["semantic_model"][0]["ai_context"] = "Plain text context"
document = OSIDocument.model_validate(document_data)
assert document.semantic_model[0].ai_context == "Plain text context"


def test_ai_context_accepts_object(document_data: dict) -> None:
document_data["semantic_model"][0]["ai_context"] = {
"instructions": "Use this model for analytics"
}
document = OSIDocument.model_validate(document_data)
ai_ctx = document.semantic_model[0].ai_context
assert isinstance(ai_ctx, OSIAIContextObject)
assert ai_ctx.instructions == "Use this model for analytics"


def test_relationship_with_alias() -> None:
relationship = OSIRelationship(
name="order_customer",
**{"from": "orders"},
to="customers",
from_columns=["customer_id"],
to_columns=["id"],
)
assert relationship.from_dataset == "orders"
assert relationship.to == "customers"


def test_relationship_with_python_name() -> None:
relationship = OSIRelationship(
name="order_customer",
from_dataset="orders",
to="customers",
from_columns=["customer_id"],
to_columns=["id"],
)
assert relationship.from_dataset == "orders"
Loading