From 29be849c6968af1f9c0a504d9acad17258615aa2 Mon Sep 17 00:00:00 2001 From: Yong Date: Thu, 23 Jul 2026 02:05:38 -0500 Subject: [PATCH 1/2] Add tests/CI for python models --- .github/workflows/python-ci.yml | 63 +++++ python/pyproject.toml | 8 + python/test/test_models.py | 466 ++++++++++++++++++++++++++++++++ python/uv.lock | 69 +++++ 4 files changed, 606 insertions(+) create mode 100644 .github/workflows/python-ci.yml create mode 100644 python/test/test_models.py diff --git a/.github/workflows/python-ci.yml b/.github/workflows/python-ci.yml new file mode 100644 index 00000000..695ad345 --- /dev/null +++ b/.github/workflows/python-ci.yml @@ -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/**' + pull_request: + branches: [ "main" ] + paths: + - 'python/**' + - '.github/**' + +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 diff --git a/python/pyproject.toml b/python/pyproject.toml index 7cc1326f..7085954e 100644 --- a/python/pyproject.toml +++ b/python/pyproject.toml @@ -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" @@ -45,3 +50,6 @@ packages = ["src/ossie"] [tool.uv] required-version = ">=0.9.0" +default-groups = [ + "dev" +] diff --git a/python/test/test_models.py b/python/test/test_models.py new file mode 100644 index 00000000..9d29b24c --- /dev/null +++ b/python/test/test_models.py @@ -0,0 +1,466 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +import json + +import pytest +import yaml +from pydantic import ValidationError + +from ossie.models import ( + OSIAIContextObject, + OSICustomExtension, + OSIDataset, + OSIDialect, + OSIDialectExpression, + OSIDimension, + OSIDocument, + OSIExpression, + OSIField, + OSIMetric, + OSIRelationship, + OSISemanticModel, + OSIVendor, +) + + +# --------------------------------------------------------------------------- +# Model specific behavior +# --------------------------------------------------------------------------- + + +def test_ai_context_object_allows_extra(): + ai_ctx = OSIAIContextObject(custom_field="custom_value") + assert ai_ctx.custom_field == "custom_value" + + +def test_ai_context_accepts_string(): + doc = OSIDocument( + semantic_model=[ + OSISemanticModel( + name="sales_model", + datasets=[OSIDataset(name="orders", source="database.schema.orders")], + ai_context="Plain text context", + ) + ] + ) + assert doc.semantic_model[0].ai_context == "Plain text context" + + +def test_ai_context_accepts_object(): + doc = OSIDocument( + semantic_model=[ + OSISemanticModel( + name="sales_model", + datasets=[OSIDataset(name="orders", source="database.schema.orders")], + ai_context={"instructions": "Use this model for analytics"}, + ) + ] + ) + ai_ctx = doc.semantic_model[0].ai_context + assert isinstance(ai_ctx, OSIAIContextObject) + assert ai_ctx.instructions == "Use this model for analytics" + + +def test_relationship_with_alias(): + 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(): + relationship = OSIRelationship( + name="order_customer", + from_dataset="orders", + to="customers", + from_columns=["customer_id"], + to_columns=["id"], + ) + assert relationship.from_dataset == "orders" + + +# --------------------------------------------------------------------------- +# Validation errors +# --------------------------------------------------------------------------- + + +def test_dataset_missing_required(): + with pytest.raises(ValidationError): + OSIDataset() + + +def test_field_missing_name(): + with pytest.raises(ValidationError): + OSIField( + expression=OSIExpression( + dialects=[OSIDialectExpression(dialect=OSIDialect.ANSI_SQL, expression="order_id")] + ) + ) + + +def test_field_missing_expression(): + with pytest.raises(ValidationError): + OSIField(name="order_id") + + +def test_relationship_missing_name(): + with pytest.raises(ValidationError): + OSIRelationship(**{"from": "orders"}, to="customers", from_columns=["customer_id"], to_columns=["id"]) + + +def test_relationship_missing_from(): + with pytest.raises(ValidationError): + OSIRelationship(name="order_customer", to="customers", from_columns=["customer_id"], to_columns=["id"]) + + +def test_relationship_missing_to(): + with pytest.raises(ValidationError): + OSIRelationship( + name="order_customer", **{"from": "orders"}, from_columns=["customer_id"], to_columns=["id"] + ) + + +def test_relationship_missing_from_columns(): + with pytest.raises(ValidationError): + OSIRelationship(name="order_customer", **{"from": "orders"}, to="customers", to_columns=["id"]) + + +def test_relationship_missing_to_columns(): + with pytest.raises(ValidationError): + OSIRelationship(name="order_customer", **{"from": "orders"}, to="customers", from_columns=["customer_id"]) + + +def test_metric_missing_name(): + with pytest.raises(ValidationError): + OSIMetric( + expression=OSIExpression( + dialects=[OSIDialectExpression(dialect=OSIDialect.ANSI_SQL, expression="order_id")] + ) + ) + + +def test_metric_missing_expression(): + with pytest.raises(ValidationError): + OSIMetric(name="total_sales") + + +def test_document_missing_semantic_model(): + with pytest.raises(ValidationError): + OSIDocument() + + +def test_semantic_model_missing_name(): + with pytest.raises(ValidationError): + OSISemanticModel(datasets=[OSIDataset(name="orders", source="database.schema.orders")]) + + +def test_semantic_model_missing_datasets(): + with pytest.raises(ValidationError): + OSISemanticModel(name="sales_model") + + +def test_custom_extension_missing_vendor_name(): + with pytest.raises(ValidationError): + OSICustomExtension(data="{}") + + +def test_custom_extension_missing_data(): + with pytest.raises(ValidationError): + OSICustomExtension(vendor_name="ASF") + + +def test_dialect_expression_missing_dialect(): + with pytest.raises(ValidationError): + OSIDialectExpression(expression="order_id") + + +def test_dialect_expression_missing_expression(): + with pytest.raises(ValidationError): + OSIDialectExpression(dialect=OSIDialect.ANSI_SQL) + + +def test_expression_missing_dialects(): + with pytest.raises(ValidationError): + OSIExpression() + + +def test_invalid_dialect_in_expression(): + with pytest.raises(ValidationError): + OSIDialectExpression(dialect="INVALID", expression="order_id") + + +# --------------------------------------------------------------------------- +# Frozen models +# --------------------------------------------------------------------------- + + +def test_frozen_dataset(): + dataset = OSIDataset(name="orders", source="database.schema.orders") + with pytest.raises(ValidationError): + dataset.name = "other" + + +def test_frozen_field(): + field = OSIField( + name="order_id", + expression=OSIExpression( + dialects=[OSIDialectExpression(dialect=OSIDialect.ANSI_SQL, expression="order_id")] + ), + ) + with pytest.raises(ValidationError): + field.name = "other" + + +def test_frozen_document(): + doc = OSIDocument( + semantic_model=[OSISemanticModel(name="sales_model", datasets=[OSIDataset(name="orders", source="database.schema.orders")])] + ) + with pytest.raises(ValidationError): + doc.version = "new.version" + + +def test_frozen_relationship(): + relationship = OSIRelationship( + name="order_customer", + **{"from": "orders"}, + to="customers", + from_columns=["customer_id"], + to_columns=["id"], + ) + with pytest.raises(ValidationError): + relationship.name = "other" + + +def test_frozen_ai_context_object(): + ai_ctx = OSIAIContextObject(instructions="Use this field for filtering") + with pytest.raises(ValidationError): + ai_ctx.instructions = "Updated instructions" + + +def test_frozen_custom_extension(): + custom_ext = OSICustomExtension(vendor_name="ASF", data="{}") + with pytest.raises(ValidationError): + custom_ext.vendor_name = "other" + + +def test_frozen_dialect_expression(): + dialect_expr = OSIDialectExpression(dialect=OSIDialect.ANSI_SQL, expression="order_id") + with pytest.raises(ValidationError): + dialect_expr.expression = "other_column" + + +def test_frozen_expression(): + expr = OSIExpression( + dialects=[OSIDialectExpression(dialect=OSIDialect.ANSI_SQL, expression="order_id")] + ) + with pytest.raises(ValidationError): + expr.dialects = [] + + +def test_frozen_dimension(): + dim = OSIDimension(is_time=True) + with pytest.raises(ValidationError): + dim.is_time = False + + +def test_frozen_metric(): + expr = OSIExpression( + dialects=[OSIDialectExpression(dialect=OSIDialect.ANSI_SQL, expression="order_id")] + ) + m = OSIMetric(name="total_sales", expression=expr) + with pytest.raises(ValidationError): + m.name = "other" + + +def test_frozen_semantic_model(): + semantic_model = OSISemanticModel(name="sales_model", datasets=[OSIDataset(name="orders", source="database.schema.orders")]) + with pytest.raises(ValidationError): + semantic_model.name = "other" + + +# --------------------------------------------------------------------------- +# Serialization +# --------------------------------------------------------------------------- + + +def test_to_osi_yaml_uses_alias(): + doc = OSIDocument( + semantic_model=[ + OSISemanticModel( + name="sales_model", + datasets=[OSIDataset(name="orders", source="database.schema.orders")], + relationships=[ + OSIRelationship( + name="order_customer", + **{"from": "orders"}, + to="customers", + from_columns=["customer_id"], + to_columns=["id"], + ) + ], + ) + ] + ) + output = doc.to_osi_yaml() + assert "from: orders" in output + assert "from_dataset" not in output + + +def test_to_osi_yaml_includes_dialects_and_vendors(): + doc = OSIDocument( + dialects=[OSIDialect.ANSI_SQL, OSIDialect.DATABRICKS], + vendors=[OSIVendor.DATABRICKS], + semantic_model=[ + OSISemanticModel( + name="sales_model", + datasets=[OSIDataset(name="orders", source="database.schema.orders")], + ) + ] + ) + output = doc.to_osi_yaml() + parsed = yaml.safe_load(output) + assert parsed["dialects"] == ["ANSI_SQL", "DATABRICKS"] + assert parsed["vendors"] == ["DATABRICKS"] + + +def test_to_osi_json_includes_dialects_and_vendors(): + doc = OSIDocument( + dialects=[OSIDialect.SNOWFLAKE], + vendors=[OSIVendor.SALESFORCE, OSIVendor.COMMON], + semantic_model=[ + OSISemanticModel( + name="sales_model", + datasets=[OSIDataset(name="orders", source="database.schema.orders")], + ) + ] + ) + output = doc.to_osi_json() + parsed = json.loads(output) + assert parsed["dialects"] == ["SNOWFLAKE"] + assert parsed["vendors"] == ["SALESFORCE", "COMMON"] + + +def test_to_osi_yaml_excludes_none(): + doc = OSIDocument( + dialects=[OSIDialect.ANSI_SQL], + semantic_model=[ + OSISemanticModel( + name="sales_model", + datasets=[OSIDataset(name="orders", source="database.schema.orders")], + ) + ] + ) + output = doc.to_osi_yaml() + parsed = yaml.safe_load(output) + assert parsed["dialects"] == ["ANSI_SQL"] + assert "vendors" not in parsed + semantic_model = parsed["semantic_model"][0] + assert semantic_model["name"] == "sales_model" + assert "description" not in semantic_model + + +def test_to_osi_json_uses_alias(): + doc = OSIDocument( + semantic_model=[ + OSISemanticModel( + name="sales_model", + datasets=[OSIDataset(name="orders", source="database.schema.orders")], + relationships=[ + OSIRelationship( + name="order_customer", + **{"from": "orders"}, + to="customers", + from_columns=["customer_id"], + to_columns=["id"], + ) + ], + ) + ] + ) + output = doc.to_osi_json() + parsed = json.loads(output) + assert parsed["semantic_model"][0]["relationships"][0]["from"] == "orders" + + +def test_to_osi_json_excludes_none(): + doc = OSIDocument( + semantic_model=[ + OSISemanticModel( + name="sales_model", + datasets=[OSIDataset(name="orders", source="database.schema.orders")], + ) + ] + ) + output = doc.to_osi_json() + parsed = json.loads(output) + semantic_model = parsed["semantic_model"][0] + assert "description" not in semantic_model + assert "relationships" not in semantic_model + assert "metrics" not in semantic_model + + +def test_to_osi_yaml_validates_as_yaml(): + doc = OSIDocument( + semantic_model=[ + OSISemanticModel( + name="sales_model", + datasets=[OSIDataset(name="orders", source="database.schema.orders")], + ) + ] + ) + out = doc.to_osi_yaml() + parsed = yaml.safe_load(out) + assert parsed["semantic_model"][0]["name"] == "sales_model" + + +def test_to_osi_json_roundtrip(): + doc = OSIDocument( + dialects=[OSIDialect.DATABRICKS], + semantic_model=[ + OSISemanticModel( + name="sales_model", + datasets=[ + OSIDataset( + name="orders", + source="database.schema.orders", + fields=[ + OSIField( + name="order_id", + expression=OSIExpression( + dialects=[ + OSIDialectExpression( + dialect=OSIDialect.DATABRICKS, expression="order_id" + ) + ] + ), + ) + ], + ) + ], + ) + ] + ) + json_str = doc.to_osi_json() + parsed = json.loads(json_str) + doc2 = OSIDocument(**parsed) + assert doc2.to_osi_json() == json_str diff --git a/python/uv.lock b/python/uv.lock index 17d1769e..a396ede5 100644 --- a/python/uv.lock +++ b/python/uv.lock @@ -20,12 +20,56 @@ dependencies = [ { name = "pyyaml" }, ] +[package.dev-dependencies] +dev = [ + { name = "pytest" }, +] + [package.metadata] requires-dist = [ { name = "pydantic", specifier = ">=2.0" }, { name = "pyyaml", specifier = ">=6.0" }, ] +[package.metadata.requires-dev] +dev = [{ name = "pytest", specifier = ">=8.0" }] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "packaging" +version = "26.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + [[package]] name = "pydantic" version = "2.13.4" @@ -143,6 +187,31 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/4b/2d/69abac8f838090bbecd5df894befb2c2619e7996a98ddb949db9f3b93225/pydantic_core-2.46.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:d51026d73fcfd93610abc7b27789c26b313920fcfb20e27462d74a7f8b06e983", size = 2193071, upload-time = "2026-05-06T13:38:08.682Z" }, ] +[[package]] +name = "pygments" +version = "2.20.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, +] + +[[package]] +name = "pytest" +version = "9.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" }, +] + [[package]] name = "pyyaml" version = "6.0.3" From b87694096045cddecd53ec33545ef7405924cf7d Mon Sep 17 00:00:00 2001 From: Yong Date: Fri, 24 Jul 2026 01:44:10 -0500 Subject: [PATCH 2/2] Refactor tests --- python/test/test_models.py | 466 ----------------------------- python/tests/conftest.py | 61 ++++ python/tests/test_models.py | 106 ++++--- python/tests/test_serialization.py | 119 ++++++++ python/tests/test_validation.py | 249 +++++++++++++++ 5 files changed, 490 insertions(+), 511 deletions(-) delete mode 100644 python/test/test_models.py create mode 100644 python/tests/conftest.py create mode 100644 python/tests/test_serialization.py create mode 100644 python/tests/test_validation.py diff --git a/python/test/test_models.py b/python/test/test_models.py deleted file mode 100644 index 9d29b24c..00000000 --- a/python/test/test_models.py +++ /dev/null @@ -1,466 +0,0 @@ -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, -# software distributed under the License is distributed on an -# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the License for the -# specific language governing permissions and limitations -# under the License. - -import json - -import pytest -import yaml -from pydantic import ValidationError - -from ossie.models import ( - OSIAIContextObject, - OSICustomExtension, - OSIDataset, - OSIDialect, - OSIDialectExpression, - OSIDimension, - OSIDocument, - OSIExpression, - OSIField, - OSIMetric, - OSIRelationship, - OSISemanticModel, - OSIVendor, -) - - -# --------------------------------------------------------------------------- -# Model specific behavior -# --------------------------------------------------------------------------- - - -def test_ai_context_object_allows_extra(): - ai_ctx = OSIAIContextObject(custom_field="custom_value") - assert ai_ctx.custom_field == "custom_value" - - -def test_ai_context_accepts_string(): - doc = OSIDocument( - semantic_model=[ - OSISemanticModel( - name="sales_model", - datasets=[OSIDataset(name="orders", source="database.schema.orders")], - ai_context="Plain text context", - ) - ] - ) - assert doc.semantic_model[0].ai_context == "Plain text context" - - -def test_ai_context_accepts_object(): - doc = OSIDocument( - semantic_model=[ - OSISemanticModel( - name="sales_model", - datasets=[OSIDataset(name="orders", source="database.schema.orders")], - ai_context={"instructions": "Use this model for analytics"}, - ) - ] - ) - ai_ctx = doc.semantic_model[0].ai_context - assert isinstance(ai_ctx, OSIAIContextObject) - assert ai_ctx.instructions == "Use this model for analytics" - - -def test_relationship_with_alias(): - 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(): - relationship = OSIRelationship( - name="order_customer", - from_dataset="orders", - to="customers", - from_columns=["customer_id"], - to_columns=["id"], - ) - assert relationship.from_dataset == "orders" - - -# --------------------------------------------------------------------------- -# Validation errors -# --------------------------------------------------------------------------- - - -def test_dataset_missing_required(): - with pytest.raises(ValidationError): - OSIDataset() - - -def test_field_missing_name(): - with pytest.raises(ValidationError): - OSIField( - expression=OSIExpression( - dialects=[OSIDialectExpression(dialect=OSIDialect.ANSI_SQL, expression="order_id")] - ) - ) - - -def test_field_missing_expression(): - with pytest.raises(ValidationError): - OSIField(name="order_id") - - -def test_relationship_missing_name(): - with pytest.raises(ValidationError): - OSIRelationship(**{"from": "orders"}, to="customers", from_columns=["customer_id"], to_columns=["id"]) - - -def test_relationship_missing_from(): - with pytest.raises(ValidationError): - OSIRelationship(name="order_customer", to="customers", from_columns=["customer_id"], to_columns=["id"]) - - -def test_relationship_missing_to(): - with pytest.raises(ValidationError): - OSIRelationship( - name="order_customer", **{"from": "orders"}, from_columns=["customer_id"], to_columns=["id"] - ) - - -def test_relationship_missing_from_columns(): - with pytest.raises(ValidationError): - OSIRelationship(name="order_customer", **{"from": "orders"}, to="customers", to_columns=["id"]) - - -def test_relationship_missing_to_columns(): - with pytest.raises(ValidationError): - OSIRelationship(name="order_customer", **{"from": "orders"}, to="customers", from_columns=["customer_id"]) - - -def test_metric_missing_name(): - with pytest.raises(ValidationError): - OSIMetric( - expression=OSIExpression( - dialects=[OSIDialectExpression(dialect=OSIDialect.ANSI_SQL, expression="order_id")] - ) - ) - - -def test_metric_missing_expression(): - with pytest.raises(ValidationError): - OSIMetric(name="total_sales") - - -def test_document_missing_semantic_model(): - with pytest.raises(ValidationError): - OSIDocument() - - -def test_semantic_model_missing_name(): - with pytest.raises(ValidationError): - OSISemanticModel(datasets=[OSIDataset(name="orders", source="database.schema.orders")]) - - -def test_semantic_model_missing_datasets(): - with pytest.raises(ValidationError): - OSISemanticModel(name="sales_model") - - -def test_custom_extension_missing_vendor_name(): - with pytest.raises(ValidationError): - OSICustomExtension(data="{}") - - -def test_custom_extension_missing_data(): - with pytest.raises(ValidationError): - OSICustomExtension(vendor_name="ASF") - - -def test_dialect_expression_missing_dialect(): - with pytest.raises(ValidationError): - OSIDialectExpression(expression="order_id") - - -def test_dialect_expression_missing_expression(): - with pytest.raises(ValidationError): - OSIDialectExpression(dialect=OSIDialect.ANSI_SQL) - - -def test_expression_missing_dialects(): - with pytest.raises(ValidationError): - OSIExpression() - - -def test_invalid_dialect_in_expression(): - with pytest.raises(ValidationError): - OSIDialectExpression(dialect="INVALID", expression="order_id") - - -# --------------------------------------------------------------------------- -# Frozen models -# --------------------------------------------------------------------------- - - -def test_frozen_dataset(): - dataset = OSIDataset(name="orders", source="database.schema.orders") - with pytest.raises(ValidationError): - dataset.name = "other" - - -def test_frozen_field(): - field = OSIField( - name="order_id", - expression=OSIExpression( - dialects=[OSIDialectExpression(dialect=OSIDialect.ANSI_SQL, expression="order_id")] - ), - ) - with pytest.raises(ValidationError): - field.name = "other" - - -def test_frozen_document(): - doc = OSIDocument( - semantic_model=[OSISemanticModel(name="sales_model", datasets=[OSIDataset(name="orders", source="database.schema.orders")])] - ) - with pytest.raises(ValidationError): - doc.version = "new.version" - - -def test_frozen_relationship(): - relationship = OSIRelationship( - name="order_customer", - **{"from": "orders"}, - to="customers", - from_columns=["customer_id"], - to_columns=["id"], - ) - with pytest.raises(ValidationError): - relationship.name = "other" - - -def test_frozen_ai_context_object(): - ai_ctx = OSIAIContextObject(instructions="Use this field for filtering") - with pytest.raises(ValidationError): - ai_ctx.instructions = "Updated instructions" - - -def test_frozen_custom_extension(): - custom_ext = OSICustomExtension(vendor_name="ASF", data="{}") - with pytest.raises(ValidationError): - custom_ext.vendor_name = "other" - - -def test_frozen_dialect_expression(): - dialect_expr = OSIDialectExpression(dialect=OSIDialect.ANSI_SQL, expression="order_id") - with pytest.raises(ValidationError): - dialect_expr.expression = "other_column" - - -def test_frozen_expression(): - expr = OSIExpression( - dialects=[OSIDialectExpression(dialect=OSIDialect.ANSI_SQL, expression="order_id")] - ) - with pytest.raises(ValidationError): - expr.dialects = [] - - -def test_frozen_dimension(): - dim = OSIDimension(is_time=True) - with pytest.raises(ValidationError): - dim.is_time = False - - -def test_frozen_metric(): - expr = OSIExpression( - dialects=[OSIDialectExpression(dialect=OSIDialect.ANSI_SQL, expression="order_id")] - ) - m = OSIMetric(name="total_sales", expression=expr) - with pytest.raises(ValidationError): - m.name = "other" - - -def test_frozen_semantic_model(): - semantic_model = OSISemanticModel(name="sales_model", datasets=[OSIDataset(name="orders", source="database.schema.orders")]) - with pytest.raises(ValidationError): - semantic_model.name = "other" - - -# --------------------------------------------------------------------------- -# Serialization -# --------------------------------------------------------------------------- - - -def test_to_osi_yaml_uses_alias(): - doc = OSIDocument( - semantic_model=[ - OSISemanticModel( - name="sales_model", - datasets=[OSIDataset(name="orders", source="database.schema.orders")], - relationships=[ - OSIRelationship( - name="order_customer", - **{"from": "orders"}, - to="customers", - from_columns=["customer_id"], - to_columns=["id"], - ) - ], - ) - ] - ) - output = doc.to_osi_yaml() - assert "from: orders" in output - assert "from_dataset" not in output - - -def test_to_osi_yaml_includes_dialects_and_vendors(): - doc = OSIDocument( - dialects=[OSIDialect.ANSI_SQL, OSIDialect.DATABRICKS], - vendors=[OSIVendor.DATABRICKS], - semantic_model=[ - OSISemanticModel( - name="sales_model", - datasets=[OSIDataset(name="orders", source="database.schema.orders")], - ) - ] - ) - output = doc.to_osi_yaml() - parsed = yaml.safe_load(output) - assert parsed["dialects"] == ["ANSI_SQL", "DATABRICKS"] - assert parsed["vendors"] == ["DATABRICKS"] - - -def test_to_osi_json_includes_dialects_and_vendors(): - doc = OSIDocument( - dialects=[OSIDialect.SNOWFLAKE], - vendors=[OSIVendor.SALESFORCE, OSIVendor.COMMON], - semantic_model=[ - OSISemanticModel( - name="sales_model", - datasets=[OSIDataset(name="orders", source="database.schema.orders")], - ) - ] - ) - output = doc.to_osi_json() - parsed = json.loads(output) - assert parsed["dialects"] == ["SNOWFLAKE"] - assert parsed["vendors"] == ["SALESFORCE", "COMMON"] - - -def test_to_osi_yaml_excludes_none(): - doc = OSIDocument( - dialects=[OSIDialect.ANSI_SQL], - semantic_model=[ - OSISemanticModel( - name="sales_model", - datasets=[OSIDataset(name="orders", source="database.schema.orders")], - ) - ] - ) - output = doc.to_osi_yaml() - parsed = yaml.safe_load(output) - assert parsed["dialects"] == ["ANSI_SQL"] - assert "vendors" not in parsed - semantic_model = parsed["semantic_model"][0] - assert semantic_model["name"] == "sales_model" - assert "description" not in semantic_model - - -def test_to_osi_json_uses_alias(): - doc = OSIDocument( - semantic_model=[ - OSISemanticModel( - name="sales_model", - datasets=[OSIDataset(name="orders", source="database.schema.orders")], - relationships=[ - OSIRelationship( - name="order_customer", - **{"from": "orders"}, - to="customers", - from_columns=["customer_id"], - to_columns=["id"], - ) - ], - ) - ] - ) - output = doc.to_osi_json() - parsed = json.loads(output) - assert parsed["semantic_model"][0]["relationships"][0]["from"] == "orders" - - -def test_to_osi_json_excludes_none(): - doc = OSIDocument( - semantic_model=[ - OSISemanticModel( - name="sales_model", - datasets=[OSIDataset(name="orders", source="database.schema.orders")], - ) - ] - ) - output = doc.to_osi_json() - parsed = json.loads(output) - semantic_model = parsed["semantic_model"][0] - assert "description" not in semantic_model - assert "relationships" not in semantic_model - assert "metrics" not in semantic_model - - -def test_to_osi_yaml_validates_as_yaml(): - doc = OSIDocument( - semantic_model=[ - OSISemanticModel( - name="sales_model", - datasets=[OSIDataset(name="orders", source="database.schema.orders")], - ) - ] - ) - out = doc.to_osi_yaml() - parsed = yaml.safe_load(out) - assert parsed["semantic_model"][0]["name"] == "sales_model" - - -def test_to_osi_json_roundtrip(): - doc = OSIDocument( - dialects=[OSIDialect.DATABRICKS], - semantic_model=[ - OSISemanticModel( - name="sales_model", - datasets=[ - OSIDataset( - name="orders", - source="database.schema.orders", - fields=[ - OSIField( - name="order_id", - expression=OSIExpression( - dialects=[ - OSIDialectExpression( - dialect=OSIDialect.DATABRICKS, expression="order_id" - ) - ] - ), - ) - ], - ) - ], - ) - ] - ) - json_str = doc.to_osi_json() - parsed = json.loads(json_str) - doc2 = OSIDocument(**parsed) - assert doc2.to_osi_json() == json_str diff --git a/python/tests/conftest.py b/python/tests/conftest.py new file mode 100644 index 00000000..1cc28ac7 --- /dev/null +++ b/python/tests/conftest.py @@ -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", + } + ], + } + ], + } diff --git a/python/tests/test_models.py b/python/tests/test_models.py index 749a226c..947ee75b 100644 --- a/python/tests/test_models.py +++ b/python/tests/test_models.py @@ -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: @@ -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] @@ -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( @@ -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" diff --git a/python/tests/test_serialization.py b/python/tests/test_serialization.py new file mode 100644 index 00000000..cef7e009 --- /dev/null +++ b/python/tests/test_serialization.py @@ -0,0 +1,119 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +import json + +import yaml + +from ossie import ( + OSIDialect, + OSIDocument, + OSIVendor, +) + + +def test_to_osi_yaml_uses_alias(document_data: dict) -> None: + document_data["semantic_model"][0]["relationships"] = [ + { + "name": "order_customer", + "from": "orders", + "to": "customers", + "from_columns": ["customer_id"], + "to_columns": ["id"], + } + ] + document = OSIDocument.model_validate(document_data) + output = document.to_osi_yaml() + assert "from: orders" in output + assert "from_dataset" not in output + + +def test_to_osi_yaml_includes_dialects_and_vendors(document_data: dict) -> None: + document_data["dialects"] = [OSIDialect.ANSI_SQL, OSIDialect.DATABRICKS] + document_data["vendors"] = [OSIVendor.DATABRICKS] + document = OSIDocument.model_validate(document_data) + output = document.to_osi_yaml() + parsed = yaml.safe_load(output) + assert parsed["dialects"] == ["ANSI_SQL", "DATABRICKS"] + assert parsed["vendors"] == ["DATABRICKS"] + + +def test_to_osi_json_includes_dialects_and_vendors(document_data: dict) -> None: + document_data["dialects"] = [OSIDialect.SNOWFLAKE] + document_data["vendors"] = [OSIVendor.SALESFORCE, OSIVendor.COMMON] + document = OSIDocument.model_validate(document_data) + output = document.to_osi_json() + parsed = json.loads(output) + assert parsed["dialects"] == ["SNOWFLAKE"] + assert parsed["vendors"] == ["SALESFORCE", "COMMON"] + + +def test_to_osi_yaml_excludes_none(document_data: dict) -> None: + document_data["dialects"] = [OSIDialect.ANSI_SQL] + document = OSIDocument.model_validate(document_data) + output = document.to_osi_yaml() + parsed = yaml.safe_load(output) + assert parsed["dialects"] == ["ANSI_SQL"] + assert "vendors" not in parsed + semantic_model = parsed["semantic_model"][0] + assert semantic_model["name"] == "typed_model" + assert "description" not in semantic_model + + +def test_to_osi_json_uses_alias(document_data: dict) -> None: + document_data["semantic_model"][0]["relationships"] = [ + { + "name": "order_customer", + "from": "orders", + "to": "customers", + "from_columns": ["customer_id"], + "to_columns": ["id"], + } + ] + document = OSIDocument.model_validate(document_data) + output = document.to_osi_json() + parsed = json.loads(output) + assert parsed["semantic_model"][0]["relationships"][0]["from"] == "orders" + + +def test_to_osi_json_excludes_none(document_data: dict) -> None: + document = OSIDocument.model_validate(document_data) + output = document.to_osi_json() + parsed = json.loads(output) + semantic_model = parsed["semantic_model"][0] + assert "description" not in semantic_model + assert "relationships" not in semantic_model + assert "vendors" not in semantic_model + + +def test_to_osi_yaml_validates_as_yaml(document_data: dict) -> None: + document = OSIDocument.model_validate(document_data) + output = document.to_osi_yaml() + parsed = yaml.safe_load(output) + assert parsed["semantic_model"][0]["name"] == "typed_model" + + +def test_to_osi_json_roundtrip(document_data: dict) -> None: + document_data["dialects"] = [OSIDialect.DATABRICKS] + document_data["semantic_model"][0]["datasets"][0]["fields"][0]["expression"] = { + "dialects": [{"dialect": OSIDialect.DATABRICKS, "expression": "order_id"}] + } + document = OSIDocument.model_validate(document_data) + json_str = document.to_osi_json() + parsed = json.loads(json_str) + document2 = OSIDocument(**parsed) + assert document2.to_osi_json() == json_str diff --git a/python/tests/test_validation.py b/python/tests/test_validation.py new file mode 100644 index 00000000..46310fcc --- /dev/null +++ b/python/tests/test_validation.py @@ -0,0 +1,249 @@ +# 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 pydantic import ValidationError + +from conftest import _expression + +from ossie import ( + OSIAIContextObject, + OSICustomExtension, + OSIDataset, + OSIDialect, + OSIDialectExpression, + OSIDimension, + OSIDocument, + OSIExpression, + OSIField, + OSIMetric, + OSIRelationship, + OSISemanticModel, +) + + +# --------------------------------------------------------------------------- +# Missing required fields +# --------------------------------------------------------------------------- + + +def test_dataset_missing_required() -> None: + with pytest.raises(ValidationError): + OSIDataset() + + +def test_field_missing_name() -> None: + with pytest.raises(ValidationError): + OSIField(expression=_expression("order_id")) + + +def test_field_missing_expression() -> None: + with pytest.raises(ValidationError): + OSIField(name="order_id") + + +def test_relationship_missing_name() -> None: + with pytest.raises(ValidationError): + OSIRelationship( + **{"from": "orders"}, + to="customers", + from_columns=["customer_id"], + to_columns=["id"], + ) + + +def test_relationship_missing_from() -> None: + with pytest.raises(ValidationError): + OSIRelationship( + name="order_customer", + to="customers", + from_columns=["customer_id"], + to_columns=["id"], + ) + + +def test_relationship_missing_to() -> None: + with pytest.raises(ValidationError): + OSIRelationship( + name="order_customer", + **{"from": "orders"}, + from_columns=["customer_id"], + to_columns=["id"], + ) + + +def test_relationship_missing_from_columns() -> None: + with pytest.raises(ValidationError): + OSIRelationship( + name="order_customer", + **{"from": "orders"}, + to="customers", + to_columns=["id"], + ) + + +def test_relationship_missing_to_columns() -> None: + with pytest.raises(ValidationError): + OSIRelationship( + name="order_customer", + **{"from": "orders"}, + to="customers", + from_columns=["customer_id"], + ) + + +def test_metric_missing_name() -> None: + with pytest.raises(ValidationError): + OSIMetric(expression=_expression("order_id")) + + +def test_metric_missing_expression() -> None: + with pytest.raises(ValidationError): + OSIMetric(name="total_sales") + + +def test_document_missing_semantic_model() -> None: + with pytest.raises(ValidationError): + OSIDocument() + + +def test_semantic_model_missing_name() -> None: + with pytest.raises(ValidationError): + OSISemanticModel( + datasets=[OSIDataset(name="orders", source="database.schema.orders")] + ) + + +def test_semantic_model_missing_datasets() -> None: + with pytest.raises(ValidationError): + OSISemanticModel(name="sales_model") + + +def test_custom_extension_missing_vendor_name() -> None: + with pytest.raises(ValidationError): + OSICustomExtension(data="{}") + + +def test_custom_extension_missing_data() -> None: + with pytest.raises(ValidationError): + OSICustomExtension(vendor_name="ASF") + + +def test_dialect_expression_missing_dialect() -> None: + with pytest.raises(ValidationError): + OSIDialectExpression(expression="order_id") + + +def test_dialect_expression_missing_expression() -> None: + with pytest.raises(ValidationError): + OSIDialectExpression(dialect=OSIDialect.ANSI_SQL) + + +def test_expression_missing_dialects() -> None: + with pytest.raises(ValidationError): + OSIExpression() + + +def test_invalid_dialect_in_expression() -> None: + with pytest.raises(ValidationError): + OSIDialectExpression(dialect="INVALID", expression="order_id") + + +# --------------------------------------------------------------------------- +# Frozen models +# --------------------------------------------------------------------------- + + +def test_frozen_dataset() -> None: + dataset = OSIDataset(name="orders", source="database.schema.orders") + with pytest.raises(ValidationError): + dataset.name = "other" + + +def test_frozen_field() -> None: + field = OSIField( + name="order_id", + expression=_expression("order_id"), + ) + with pytest.raises(ValidationError): + field.name = "other" + + +def test_frozen_document(document_data: dict) -> None: + document = OSIDocument.model_validate(document_data) + with pytest.raises(ValidationError): + document.version = "new.version" + + +def test_frozen_relationship() -> None: + relationship = OSIRelationship( + name="order_customer", + **{"from": "orders"}, + to="customers", + from_columns=["customer_id"], + to_columns=["id"], + ) + with pytest.raises(ValidationError): + relationship.name = "other" + + +def test_frozen_ai_context_object() -> None: + ai_ctx = OSIAIContextObject(instructions="Use this field for filtering") + with pytest.raises(ValidationError): + ai_ctx.instructions = "Updated instructions" + + +def test_frozen_custom_extension() -> None: + custom_ext = OSICustomExtension(vendor_name="ASF", data="{}") + with pytest.raises(ValidationError): + custom_ext.vendor_name = "other" + + +def test_frozen_dialect_expression() -> None: + dialect_expr = OSIDialectExpression( + dialect=OSIDialect.ANSI_SQL, expression="order_id" + ) + with pytest.raises(ValidationError): + dialect_expr.expression = "other_column" + + +def test_frozen_expression() -> None: + expr = _expression("order_id") + with pytest.raises(ValidationError): + expr.dialects = [] + + +def test_frozen_dimension() -> None: + dim = OSIDimension(is_time=True) + with pytest.raises(ValidationError): + dim.is_time = False + + +def test_frozen_metric() -> None: + expr = _expression("order_id") + m = OSIMetric(name="total_sales", expression=expr) + with pytest.raises(ValidationError): + m.name = "other" + + +def test_frozen_semantic_model() -> None: + semantic_model = OSISemanticModel( + name="sales_model", + datasets=[OSIDataset(name="orders", source="database.schema.orders")], + ) + with pytest.raises(ValidationError): + semantic_model.name = "other"