From f4b83503bf044c83b270bbec5405fdd599fe8fed Mon Sep 17 00:00:00 2001 From: Dragos Crintea Date: Wed, 15 Jul 2026 14:16:05 +0100 Subject: [PATCH 1/6] add semantido as a ossie vendor --- python/src/ossie/models.py | 1 + 1 file changed, 1 insertion(+) diff --git a/python/src/ossie/models.py b/python/src/ossie/models.py index b0db1cc0..1c8b6465 100644 --- a/python/src/ossie/models.py +++ b/python/src/ossie/models.py @@ -43,6 +43,7 @@ class OSIVendor(str, Enum): DBT = "DBT" DATABRICKS = "DATABRICKS" GOODDATA = "GOODDATA" + SEMANTIDO = "SEMANTIDO" class OSIAIContextObject(BaseModel): From bc65bdd28053cbc618f8d8499c05b570d40bc691 Mon Sep 17 00:00:00 2001 From: Dragos Crintea Date: Sat, 18 Jul 2026 23:03:52 +0100 Subject: [PATCH 2/6] semantido <-> ossie converter --- converters/semantido/README.md | 86 +++++++ converters/semantido/pyproject.toml | 53 ++++ .../semantido/src/ossie_semantido/__init__.py | 32 +++ .../semantido/src/ossie_semantido/cli.py | 87 +++++++ .../src/ossie_semantido/converter_issues.py | 51 ++++ .../semantido/src/ossie_semantido/loaders.py | 46 ++++ .../src/ossie_semantido/osi_to_semantido.py | 153 ++++++++++++ .../src/ossie_semantido/semantido_to_osi.py | 190 +++++++++++++++ converters/semantido/tests/__init__.py | 0 .../semantido/tests/fixtures/__init__.py | 0 .../tests/fixtures/emir_reporting/__init__.py | 0 .../emir_reporting/models/__init__.py | 0 .../emir_reporting/models/emir_reporting.py | 226 ++++++++++++++++++ converters/semantido/tests/helpers.py | 30 +++ .../semantido/tests/test_osi_to_semantido.py | 37 +++ .../semantido/tests/test_semantido_to_osi.py | 76 ++++++ 16 files changed, 1067 insertions(+) create mode 100644 converters/semantido/README.md create mode 100644 converters/semantido/pyproject.toml create mode 100644 converters/semantido/src/ossie_semantido/__init__.py create mode 100644 converters/semantido/src/ossie_semantido/cli.py create mode 100644 converters/semantido/src/ossie_semantido/converter_issues.py create mode 100644 converters/semantido/src/ossie_semantido/loaders.py create mode 100644 converters/semantido/src/ossie_semantido/osi_to_semantido.py create mode 100644 converters/semantido/src/ossie_semantido/semantido_to_osi.py create mode 100644 converters/semantido/tests/__init__.py create mode 100644 converters/semantido/tests/fixtures/__init__.py create mode 100644 converters/semantido/tests/fixtures/emir_reporting/__init__.py create mode 100644 converters/semantido/tests/fixtures/emir_reporting/models/__init__.py create mode 100644 converters/semantido/tests/fixtures/emir_reporting/models/emir_reporting.py create mode 100644 converters/semantido/tests/helpers.py create mode 100644 converters/semantido/tests/test_osi_to_semantido.py create mode 100644 converters/semantido/tests/test_semantido_to_osi.py diff --git a/converters/semantido/README.md b/converters/semantido/README.md new file mode 100644 index 00000000..fb11b97b --- /dev/null +++ b/converters/semantido/README.md @@ -0,0 +1,86 @@ + + +# 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` / `_description` | `description` | | +| `business_context`, `application_context` | dataset `ai_context.instructions` | | +| `synonyms` / `_synonyms` | `ai_context.synonyms` | | +| `_sample_values` | field `ai_context.examples` | | +| `time_dimension=` + `_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) | +| `_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/ +``` diff --git a/converters/semantido/pyproject.toml b/converters/semantido/pyproject.toml new file mode 100644 index 00000000..89f5d54e --- /dev/null +++ b/converters/semantido/pyproject.toml @@ -0,0 +1,53 @@ +# 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.0", + "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", +] + +[tool.uv.sources] +apache-ossie = { path = "../../python", editable = true } diff --git a/converters/semantido/src/ossie_semantido/__init__.py b/converters/semantido/src/ossie_semantido/__init__.py new file mode 100644 index 00000000..8a4f9377 --- /dev/null +++ b/converters/semantido/src/ossie_semantido/__init__.py @@ -0,0 +1,32 @@ +# 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", +] diff --git a/converters/semantido/src/ossie_semantido/cli.py b/converters/semantido/src/ossie_semantido/cli.py new file mode 100644 index 00000000..7e26df68 --- /dev/null +++ b/converters/semantido/src/ossie_semantido/cli.py @@ -0,0 +1,87 @@ +# 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() diff --git a/converters/semantido/src/ossie_semantido/converter_issues.py b/converters/semantido/src/ossie_semantido/converter_issues.py new file mode 100644 index 00000000..e35fbfb2 --- /dev/null +++ b/converters/semantido/src/ossie_semantido/converter_issues.py @@ -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] diff --git a/converters/semantido/src/ossie_semantido/loaders.py b/converters/semantido/src/ossie_semantido/loaders.py new file mode 100644 index 00000000..eba7c85a --- /dev/null +++ b/converters/semantido/src/ossie_semantido/loaders.py @@ -0,0 +1,46 @@ +# 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. + +"""Entry points for loading a semantido SemanticLayer. + +semantido's source of truth is live Python (decorated SQLAlchemy models), +so the primary loader imports a module and syncs the layer. A file-based +path is not provided in this converter: semantido's JSON export is a +rendering of the same layer, and consumers who have the JSON already have +a semantic model they can transform directly. +""" + +import importlib +import sys +from pathlib import Path + +from semantido import SemanticDeclarativeBase +from semantido.generators.semantic_layer import SemanticLayer + + +def load_from_module(module_path: str, sys_path: str | None = None) -> SemanticLayer: + """Import a module of decorated models and return the synced layer. + + Args: + module_path: Dotted module path, e.g. ``models.emir_reporting``. + sys_path: Optional directory prepended to ``sys.path`` first, so + model packages can be loaded without installation. + """ + if sys_path: + sys.path.insert(0, str(Path(sys_path).resolve())) + importlib.import_module(module_path) + return SemanticDeclarativeBase.sync_semantic_layer() diff --git a/converters/semantido/src/ossie_semantido/osi_to_semantido.py b/converters/semantido/src/ossie_semantido/osi_to_semantido.py new file mode 100644 index 00000000..91a4aa2c --- /dev/null +++ b/converters/semantido/src/ossie_semantido/osi_to_semantido.py @@ -0,0 +1,153 @@ +# 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. + +"""Apache Ossie document -> generated semantido model code. + +This direction is code generation: it emits a Python module of +``@semantic_table``-decorated SQLAlchemy models. Constructs with no +semantido equivalent (metrics, non-ANSI dialect expressions) are dropped +with a recorded ConverterIssue and listed in a TODO comment block at the +top of the generated file. +""" + +import json +from typing import List + +from ossie import OSIDataset, OSIDocument, OSIField + +from ossie_semantido.converter_issues import ConverterIssue, ConverterIssueType, ConverterResult + +VENDOR_NAME = "SEMANTIDO" + +_TYPE_MAP = { + "VARCHAR": "String", + "CHAR": "String", + "TEXT": "String", + "INTEGER": "Integer", + "BIGINT": "Integer", + "SMALLINT": "Integer", + "NUMERIC": "Numeric", + "DECIMAL": "Numeric", + "FLOAT": "Float", + "DATE": "Date", + "DATETIME": "DateTime", + "TIMESTAMP": "DateTime", + "BOOLEAN": "Boolean", +} + + +def _vendor_payload(entity) -> dict: + for ext in entity.custom_extensions or []: + if ext.vendor_name == VENDOR_NAME: + try: + return json.loads(ext.data) + except (TypeError, ValueError): + return {} + return {} + + +def _sqlalchemy_type(field: OSIField, issues: List[ConverterIssue]) -> str: + data_type = _vendor_payload(field).get("data_type", "") + base = data_type.split("(")[0].strip().upper() + if base in _TYPE_MAP: + return _TYPE_MAP[base] + if base: + issues.append( + ConverterIssue(ConverterIssueType.UNMAPPED_DATA_TYPE, f"{field.name}:{base}") + ) + return "String" + + +def _ai(entity): + ctx = entity.ai_context + if ctx is None or isinstance(ctx, str): + return None + return ctx + + +def _class_name(dataset_name: str) -> str: + return "".join(part.capitalize() for part in dataset_name.split("_")) + + +def _emit_dataset(dataset: OSIDataset, issues: List[ConverterIssue]) -> str: + payload = _vendor_payload(dataset) + ctx = _ai(dataset) + + decorator_kwargs = [] + if dataset.description: + decorator_kwargs.append(f" description={dataset.description!r},") + if ctx and ctx.instructions: + decorator_kwargs.append(f" business_context={ctx.instructions!r},") + if ctx and ctx.synonyms: + decorator_kwargs.append(f" synonyms={list(ctx.synonyms)!r},") + if payload.get("sql_filters"): + decorator_kwargs.append(f" sql_filters={payload['sql_filters']!r},") + if payload.get("time_dimension"): + decorator_kwargs.append(f" time_dimension={payload['time_dimension']!r},") + + lines = ["@semantic_table("] + decorator_kwargs + [")"] + lines.append(f"class {_class_name(dataset.name)}(SemanticDeclarativeBase):") + lines.append(f' __tablename__ = "{dataset.name}"') + lines.append("") + + primary_keys = set(dataset.primary_key or []) + for field in dataset.fields or []: + field_payload = _vendor_payload(field) + field_ctx = _ai(field) + sa_type = _sqlalchemy_type(field, issues) + pk = ", primary_key=True" if field.name in primary_keys else "" + lines.append(f" {field.name} = Column({sa_type}{pk})") + if field.description: + lines.append(f" {field.name}_description = {field.description!r}") + if field_payload.get("privacy_level"): + level = field_payload["privacy_level"].upper() + lines.append(f" {field.name}_privacy_level = PrivacyLevel.{level}") + if field_ctx and field_ctx.examples: + lines.append(f" {field.name}_sample_values = {[str(e) for e in field_ctx.examples]!r}") + if field_payload.get("time_grain"): + grain = field_payload["time_grain"].upper() + lines.append(f" {field.name}_time_grain = TimeGrain.{grain}") + return "\n".join(lines) + + +def osi_to_semantido_source(document: OSIDocument) -> ConverterResult[str]: + """Generate semantido model source code from an Ossie document.""" + issues: List[ConverterIssue] = [] + blocks = [] + + for model in document.semantic_model: + for metric in model.metrics or []: + issues.append(ConverterIssue(ConverterIssueType.METRIC_DROPPED, metric.name)) + for dataset in model.datasets: + blocks.append(_emit_dataset(dataset, issues)) + + todo_block = "" + if issues: + todo_lines = "\n".join(f"# {i.issue_type.value}: {i.element_name}" for i in issues) + todo_block = ( + "# TODO(ossie-semantido): the following Ossie constructs have no\n" + "# semantido equivalent yet and were not converted:\n" + todo_lines + "\n\n" + ) + + header = ( + '"""semantido models generated by ossie-semantido. Review before use."""\n\n' + "from sqlalchemy import Boolean, Column, Date, DateTime, Float, Integer, Numeric, String\n\n" + "from semantido import SemanticDeclarativeBase, semantic_table\n" + "from semantido.generators.semantic_layer import PrivacyLevel, TimeGrain\n\n\n" + ) + source = header + todo_block + "\n\n\n".join(blocks) + "\n" + return ConverterResult(output=source, issues=issues) diff --git a/converters/semantido/src/ossie_semantido/semantido_to_osi.py b/converters/semantido/src/ossie_semantido/semantido_to_osi.py new file mode 100644 index 00000000..74743b85 --- /dev/null +++ b/converters/semantido/src/ossie_semantido/semantido_to_osi.py @@ -0,0 +1,190 @@ +# 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 SemanticLayer -> Apache Ossie document conversion. + +Builds the Ossie model through the typed ``apache-ossie`` objects so the +output is schema-conformant by construction. Governance metadata that has +no Ossie core field (privacy levels, SQL filters, time grain, the primary +time axis) is preserved losslessly in ``custom_extensions`` under the +``SEMANTIDO`` vendor name, with ``data`` as a serialized JSON string as +the specification requires. +""" + +import json +import re +from typing import List, Optional + +from ossie import ( + OSIAIContextObject, + OSICustomExtension, + OSIDataset, + OSIDialect, + OSIDialectExpression, + OSIDimension, + OSIDocument, + OSIExpression, + OSIField, + OSIRelationship, + OSISemanticModel, +) +from semantido.generators.semantic_layer import Column, Relationship, SemanticLayer, Table + +from ossie_semantido.converter_issues import ConverterIssue, ConverterIssueType, ConverterResult + +VENDOR_NAME = "SEMANTIDO" + +_JOIN_RE = re.compile( + r"^\s*(\w+)\.(\w+)\s*=\s*(\w+)\.(\w+)\s*$" +) + + +def _extension(payload: dict) -> OSICustomExtension: + """Wrap semantido-specific metadata as a spec-conformant custom extension.""" + return OSICustomExtension( + vendor_name=VENDOR_NAME, + data=json.dumps(payload, sort_keys=True, default=str), + ) + + +def _ansi(expression: str) -> OSIExpression: + return OSIExpression( + dialects=[OSIDialectExpression(dialect=OSIDialect.ANSI_SQL, expression=expression)] + ) + + +def _column_to_field(column: Column) -> OSIField: + ai_kwargs = {} + if column.synonyms: + ai_kwargs["synonyms"] = tuple(column.synonyms) + if column.sample_values: + ai_kwargs["examples"] = tuple(str(v) for v in column.sample_values) + if column.application_rules: + ai_kwargs["instructions"] = " ".join(column.application_rules) + + extension_payload = {} + if column.privacy_level is not None: + extension_payload["privacy_level"] = column.privacy_level.value + if column.time_grain is not None: + extension_payload["time_grain"] = column.time_grain.value + if column.data_type: + extension_payload["data_type"] = column.data_type + if column.references: + extension_payload["references"] = column.references + + return OSIField( + name=column.name, + expression=_ansi(column.name), + dimension=OSIDimension(is_time=True) if column.is_time_dimension else None, + description=column.description or None, + ai_context=OSIAIContextObject(**ai_kwargs) if ai_kwargs else None, + custom_extensions=[_extension(extension_payload)] if extension_payload else None, + ) + + +def _table_to_dataset(table: Table) -> OSIDataset: + instructions_parts = [p for p in (table.business_context, table.application_context) if p] + + ai_kwargs = {} + if instructions_parts: + ai_kwargs["instructions"] = " ".join(instructions_parts) + if table.synonyms: + ai_kwargs["synonyms"] = tuple(table.synonyms) + + extension_payload = {} + if table.sql_filters: + extension_payload["sql_filters"] = table.sql_filters + if table.time_dimension: + extension_payload["time_dimension"] = table.time_dimension + + return OSIDataset( + name=table.name, + source=f"{table.schema}.{table.name}" if table.schema else table.name, + primary_key=table.primary_key if table.primary_key else None, + description=table.description or None, + ai_context=OSIAIContextObject(**ai_kwargs) if ai_kwargs else None, + fields=[_column_to_field(c) for c in table.columns] or None, + custom_extensions=[_extension(extension_payload)] if extension_payload else None, + ) + + +def _relationship_to_osi( + rel: Relationship, issues: List[ConverterIssue] +) -> Optional[OSIRelationship]: + match = _JOIN_RE.match(rel.join_condition or "") + if not match: + issues.append( + ConverterIssue( + issue_type=ConverterIssueType.RELATIONSHIP_JOIN_UNPARSED, + element_name=f"{rel.from_table}->{rel.to_table}", + ) + ) + return None + + left_table, left_col, right_table, right_col = match.groups() + # Normalize so from/to match the declared direction regardless of + # which side of the equality each table appeared on. + if left_table == rel.from_table: + from_columns, to_columns = [left_col], [right_col] + else: + from_columns, to_columns = [right_col], [left_col] + + ai_kwargs = {} + if rel.description: + ai_kwargs["instructions"] = rel.description + + return OSIRelationship.model_validate( + { + "name": f"{rel.from_table}_to_{rel.to_table}", + "from": rel.from_table, + "to": rel.to_table, + "from_columns": from_columns, + "to_columns": to_columns, + "ai_context": ai_kwargs or None, + "custom_extensions": [ + _extension({"relationship_type": rel.relationship_type.value}).model_dump() + ], + } + ) + + +def semantic_layer_to_osi( + layer: SemanticLayer, model_name: str, description: Optional[str] = None +) -> ConverterResult[OSIDocument]: + """Convert a synced semantido SemanticLayer into an Ossie document.""" + issues: List[ConverterIssue] = [] + + datasets = [_table_to_dataset(t) for t in layer.tables.values()] + + relationships = [] + for rel in layer.relationships: + converted = _relationship_to_osi(rel, issues) + if converted is not None: + relationships.append(converted) + + model_extensions = None + if layer.application_glossary: + model_extensions = [_extension({"application_glossary": layer.application_glossary})] + + model = OSISemanticModel( + name=model_name, + description=description, + datasets=datasets, + relationships=relationships or None, + custom_extensions=model_extensions, + ) + return ConverterResult(output=OSIDocument(semantic_model=[model]), issues=issues) diff --git a/converters/semantido/tests/__init__.py b/converters/semantido/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/converters/semantido/tests/fixtures/__init__.py b/converters/semantido/tests/fixtures/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/converters/semantido/tests/fixtures/emir_reporting/__init__.py b/converters/semantido/tests/fixtures/emir_reporting/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/converters/semantido/tests/fixtures/emir_reporting/models/__init__.py b/converters/semantido/tests/fixtures/emir_reporting/models/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/converters/semantido/tests/fixtures/emir_reporting/models/emir_reporting.py b/converters/semantido/tests/fixtures/emir_reporting/models/emir_reporting.py new file mode 100644 index 00000000..2485e3af --- /dev/null +++ b/converters/semantido/tests/fixtures/emir_reporting/models/emir_reporting.py @@ -0,0 +1,226 @@ +"""EMIR trade-reporting semantic model (regulated-industry fixture). + +Simplified but structurally honest EMIR REFIT reporting schema. It +deliberately encodes three semantics-dependent failure modes that plain +DDL cannot express: bridge fan-out via the counterparty role table, sign +conventions (signed valuations vs. unsigned posted/received collateral), +and amount ambiguity (notional vs. valuation vs. collateral). +""" + +from sqlalchemy import Column, Date, DateTime, ForeignKey, Integer, Numeric, String +from sqlalchemy.orm import relationship + +from semantido import SemanticDeclarativeBase, semantic_table +from semantido.generators.semantic_layer import PrivacyLevel, TimeGrain + + +@semantic_table( + description="Legal entity master for derivative counterparties, one row per LEI.", + business_context=( + "EMIR counterparty classification drives clearing and reporting " + "obligations (FC / NFC+ / NFC- under Art. 2 and Art. 10)." + ), + synonyms=["legal entities", "counterparties"], +) +class Counterparty(SemanticDeclarativeBase): + __tablename__ = "counterparty" + + lei = Column(String(20), primary_key=True) + lei_description = ( + "Legal Entity Identifier (ISO 17442). The sole join key for " + "counterparty identity everywhere in this schema." + ) + lei_sample_values = ["529900T8BM49AURSDO55"] + + legal_name = Column(String(200), nullable=False) + legal_name_description = "Registered legal name as per GLEIF record." + legal_name_privacy_level = PrivacyLevel.CONFIDENTIAL + + classification = Column(String(4), nullable=False) + classification_description = ( + "EMIR counterparty classification: 'FC' (financial counterparty, Art. 2(8)), " + "'NFC+' (non-financial above clearing threshold, Art. 10), 'NFC-' (below). " + "Filter on this before any obligation question." + ) + classification_sample_values = ["FC", "NFC+"] + + country = Column(String(2), nullable=False) + country_description = "ISO 3166-1 alpha-2 country of incorporation." + + roles = relationship("CounterpartyTradeRole", back_populates="counterparty") + roles_relationship_description = "Roles this entity plays on trades." + + +@semantic_table( + description=( + "Reportable OTC derivative trades, one row per UTI. Immutable birth " + "record of the trade; evolving economics live in trade_state." + ), + business_context=( + "AMBIGUITY GUARD: notional_amount is contract size, NOT current value " + "and NOT exposure. 'How big' -> notional; 'worth today' -> " + "trade_state.valuation_amount; 'at risk' -> collateral_report." + ), + synonyms=["trades", "derivatives", "positions"], + time_dimension="execution_timestamp", +) +class Trade(SemanticDeclarativeBase): + __tablename__ = "trade" + + uti = Column(String(52), primary_key=True) + uti_description = "Unique Trade Identifier (Art. 9 REFIT). Canonical trade key." + + asset_class = Column(String(4), nullable=False) + asset_class_description = ( + "EMIR asset class: 'IR' rates, 'CR' credit, 'EQ' equity, 'FX' FX, 'CO' commodity." + ) + asset_class_sample_values = ["IR", "FX"] + + notional_amount = Column(Numeric(20, 2), nullable=False) + notional_amount_description = ( + "Trade notional in notional_currency. Contract size, not value. Never " + "sum across currencies without conversion." + ) + notional_amount_privacy_level = PrivacyLevel.RESTRICTED + + notional_currency = Column(String(3), nullable=False) + notional_currency_description = "ISO 4217 currency of notional_amount." + notional_currency_sample_values = ["EUR", "USD"] + + execution_timestamp = Column(DateTime, nullable=False) + execution_timestamp_description = "UTC execution time (Art. 9 REFIT). Primary time axis." + execution_timestamp_time_grain = TimeGrain.SECOND + + states = relationship("TradeState", back_populates="trade") + states_relationship_description = "Daily state reports for this trade." + roles = relationship("CounterpartyTradeRole", back_populates="trade") + roles_relationship_description = "Counterparty roles attached to this trade." + + +@semantic_table( + description=( + "Daily trade-state reports (EMIR REFIT is state-based), one row per " + "UTI per reporting_date. The latest state per UTI is the current view." + ), + business_context=( + "SIGN CONVENTION: valuation_amount is signed from the REPORTING " + "counterparty's perspective. Aggregating across counterparties without " + "flipping signs by role is meaningless." + ), + sql_filters=[ + "reporting_date = (SELECT MAX(ts2.reporting_date) FROM trade_state ts2 " + "WHERE ts2.uti = trade_state.uti)" + ], + time_dimension="reporting_date", +) +class TradeState(SemanticDeclarativeBase): + __tablename__ = "trade_state" + + id = Column(Integer, primary_key=True) + uti = Column(String(52), ForeignKey("trade.uti"), nullable=False) + reporting_date = Column(Date, nullable=False) + reporting_date_description = "State date; one state per trade per day." + reporting_date_time_grain = TimeGrain.DAY + + valuation_amount = Column(Numeric(20, 2)) + valuation_amount_description = ( + "Mark-to-market value (Art. 11(2)), signed from the reporting " + "counterparty's perspective. Value, not size: do not confuse with notional." + ) + valuation_amount_privacy_level = PrivacyLevel.RESTRICTED + + contract_status = Column(String(4), nullable=False) + contract_status_description = ( + "'OUTS' outstanding, 'TERM' terminated, 'MATU' matured, 'ERRO' error. " + "Exclude non-OUTS from exposure aggregates unless asked otherwise." + ) + contract_status_sample_values = ["OUTS", "TERM"] + + trade = relationship("Trade", back_populates="states") + trade_relationship_description = "The trade this state report belongs to." + + +@semantic_table( + description=( + "Role bridge between trades and counterparties. A trade has AT LEAST " + "two rows here (reporting + other counterparty), often more." + ), + business_context=( + "FAN-OUT GUARD: joining trade -> this table -> counterparty multiplies " + "trade rows by role count; any SUM over trade amounts after this join " + "double-counts. Filter to a single role (usually 'RPTG') before " + "aggregating trade-level amounts." + ), +) +class CounterpartyTradeRole(SemanticDeclarativeBase): + __tablename__ = "counterparty_trade_role" + + id = Column(Integer, primary_key=True) + uti = Column(String(52), ForeignKey("trade.uti"), nullable=False) + lei = Column(String(20), ForeignKey("counterparty.lei"), nullable=False) + + role = Column(String(4), nullable=False) + role_description = ( + "'RPTG' reporting counterparty, 'OTHR' other counterparty, 'BRKR' " + "broker, 'CLRM' clearing member. Exactly one RPTG row per UTI." + ) + role_sample_values = ["RPTG", "OTHR"] + + trade = relationship("Trade", back_populates="roles") + trade_relationship_description = "Trade this role row belongs to." + counterparty = relationship("Counterparty", back_populates="roles") + counterparty_relationship_description = "Entity playing this role." + + +@semantic_table( + description="Daily collateral/margin per UTI (Art. 11(3)).", + business_context=( + "SIGN CONVENTION (different from trade_state!): amounts are UNSIGNED; " + "direction is carried by which column they sit in. Net collateral = " + "posted - received, computed, never read from a column." + ), + time_dimension="reporting_date", +) +class CollateralReport(SemanticDeclarativeBase): + __tablename__ = "collateral_report" + + id = Column(Integer, primary_key=True) + uti = Column(String(52), ForeignKey("trade.uti"), nullable=False) + reporting_date = Column(Date, nullable=False) + reporting_date_time_grain = TimeGrain.DAY + + initial_margin_posted = Column(Numeric(20, 2)) + initial_margin_posted_description = "IM posted BY the reporting counterparty. Unsigned." + initial_margin_received = Column(Numeric(20, 2)) + initial_margin_received_description = ( + "IM received. Unsigned; do not sum with posted — subtract." + ) + collateral_currency = Column(String(3)) + collateral_currency_description = "ISO 4217 currency of margin amounts." + + +@semantic_table( + description=( + "Trade repository submission log. Grain: one row per submission " + "attempt — a trade may have many." + ), + business_context=( + "Rejection-rate questions resolve here, per attempt, not per trade, " + "unless deduplicated. Rejection rate = NACK / all attempts." + ), + time_dimension="submission_timestamp", +) +class SubmissionLog(SemanticDeclarativeBase): + __tablename__ = "submission_log" + + id = Column(Integer, primary_key=True) + uti = Column(String(52), ForeignKey("trade.uti"), nullable=False) + submission_timestamp = Column(DateTime, nullable=False) + submission_timestamp_time_grain = TimeGrain.SECOND + + status = Column(String(4), nullable=False) + status_description = "'ACKD' accepted, 'NACK' rejected, 'PEND' pending." + status_sample_values = ["ACKD", "NACK"] + + nack_reason = Column(String(200)) + nack_reason_description = "TR rejection reason text; NULL unless NACK." diff --git a/converters/semantido/tests/helpers.py b/converters/semantido/tests/helpers.py new file mode 100644 index 00000000..d7093dce --- /dev/null +++ b/converters/semantido/tests/helpers.py @@ -0,0 +1,30 @@ +# 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 test helpers.""" + +from pathlib import Path + +from ossie_semantido.loaders import load_from_module + +FIXTURES = Path(__file__).parent / "fixtures" + + +def load_emir_layer(): + return load_from_module( + "models.emir_reporting", sys_path=str(FIXTURES / "emir_reporting") + ) diff --git a/converters/semantido/tests/test_osi_to_semantido.py b/converters/semantido/tests/test_osi_to_semantido.py new file mode 100644 index 00000000..b03f8ad1 --- /dev/null +++ b/converters/semantido/tests/test_osi_to_semantido.py @@ -0,0 +1,37 @@ +# 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. + +from ossie_semantido.osi_to_semantido import osi_to_semantido_source +from ossie_semantido.semantido_to_osi import semantic_layer_to_osi +from tests.helpers import load_emir_layer + + +def test_generated_source_compiles(): + layer = load_emir_layer() + document = semantic_layer_to_osi(layer, model_name="emir_reporting").output + result = osi_to_semantido_source(document) + compile(result.output, "", "exec") + + +def test_roundtrip_preserves_governance_annotations(): + layer = load_emir_layer() + document = semantic_layer_to_osi(layer, model_name="emir_reporting").output + source = osi_to_semantido_source(document).output + assert "sql_filters=" in source + assert "PrivacyLevel.CONFIDENTIAL" in source + assert 'time_dimension="reporting_date"' in source or "time_dimension='reporting_date'" in source + assert "TimeGrain.DAY" in source diff --git a/converters/semantido/tests/test_semantido_to_osi.py b/converters/semantido/tests/test_semantido_to_osi.py new file mode 100644 index 00000000..4b65a666 --- /dev/null +++ b/converters/semantido/tests/test_semantido_to_osi.py @@ -0,0 +1,76 @@ +# 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 + +from ossie import OSIDocument + +from ossie_semantido.semantido_to_osi import VENDOR_NAME, semantic_layer_to_osi +from tests.helpers import load_emir_layer + + +def _vendor_data(entity): + for ext in entity.custom_extensions or []: + if ext.vendor_name == VENDOR_NAME: + return json.loads(ext.data) + return {} + + +def test_converts_all_tables_to_datasets(): + layer = load_emir_layer() + result = semantic_layer_to_osi(layer, model_name="emir_reporting") + model = result.output.semantic_model[0] + assert {d.name for d in model.datasets} == set(layer.tables) + + +def test_relationships_are_parsed_not_dropped(): + layer = load_emir_layer() + result = semantic_layer_to_osi(layer, model_name="emir_reporting") + model = result.output.semantic_model[0] + assert model.relationships, "expected FK relationships in the fixture" + assert not result.issues, f"unexpected conversion issues: {result.issues}" + rel = model.relationships[0] + assert rel.from_columns and rel.to_columns + + +def test_governance_metadata_preserved_in_extensions(): + layer = load_emir_layer() + model = semantic_layer_to_osi(layer, model_name="emir_reporting").output.semantic_model[0] + + trade_state = next(d for d in model.datasets if d.name == "trade_state") + assert _vendor_data(trade_state).get("sql_filters"), "sql_filters must survive" + assert _vendor_data(trade_state).get("time_dimension") == "reporting_date" + + counterparty = next(d for d in model.datasets if d.name == "counterparty") + legal_name = next(f for f in counterparty.fields if f.name == "legal_name") + assert _vendor_data(legal_name).get("privacy_level") == "confidential" + + +def test_time_dimension_marked_on_field(): + layer = load_emir_layer() + model = semantic_layer_to_osi(layer, model_name="emir_reporting").output.semantic_model[0] + trade = next(d for d in model.datasets if d.name == "trade") + ts_field = next(f for f in trade.fields if f.name == "execution_timestamp") + assert ts_field.dimension is not None and ts_field.dimension.is_time is True + + +def test_yaml_output_is_valid_document(): + layer = load_emir_layer() + yaml_text = semantic_layer_to_osi(layer, model_name="emir_reporting").output.to_osi_yaml() + import yaml as pyyaml + + OSIDocument.model_validate(pyyaml.safe_load(yaml_text)) From bb04eb39cce988a20ca845ad2633b5ed2a4f5848 Mon Sep 17 00:00:00 2001 From: Dragos Crintea Date: Sat, 18 Jul 2026 23:15:25 +0100 Subject: [PATCH 3/6] semantido <-> ossie converter --- examples/semantido_to_osi_emir_reporting.yaml | 464 ++++++++++++++++++ 1 file changed, 464 insertions(+) create mode 100644 examples/semantido_to_osi_emir_reporting.yaml diff --git a/examples/semantido_to_osi_emir_reporting.yaml b/examples/semantido_to_osi_emir_reporting.yaml new file mode 100644 index 00000000..582334b3 --- /dev/null +++ b/examples/semantido_to_osi_emir_reporting.yaml @@ -0,0 +1,464 @@ +version: 0.2.0.dev0 +semantic_model: +- name: emir_reporting + datasets: + - name: collateral_report + source: collateral_report + primary_key: + - id + description: Daily collateral/margin per UTI (Art. 11(3)). + ai_context: + instructions: 'SIGN CONVENTION (different from trade_state!): amounts are UNSIGNED; + direction is carried by which column they sit in. Net collateral = posted + - received, computed, never read from a column.' + fields: + - name: id + expression: + dialects: + - dialect: ANSI_SQL + expression: id + description: 'Column: id' + custom_extensions: + - vendor_name: SEMANTIDO + data: '{"data_type": "INTEGER"}' + - name: uti + expression: + dialects: + - dialect: ANSI_SQL + expression: uti + description: 'Column: uti' + custom_extensions: + - vendor_name: SEMANTIDO + data: '{"data_type": "VARCHAR", "references": "trade.uti"}' + - name: reporting_date + expression: + dialects: + - dialect: ANSI_SQL + expression: reporting_date + dimension: + is_time: true + description: 'Column: reporting_date' + custom_extensions: + - vendor_name: SEMANTIDO + data: '{"data_type": "DATE", "time_grain": "day"}' + - name: initial_margin_posted + expression: + dialects: + - dialect: ANSI_SQL + expression: initial_margin_posted + description: IM posted BY the reporting counterparty. Unsigned. + custom_extensions: + - vendor_name: SEMANTIDO + data: '{"data_type": "DECIMAL"}' + - name: initial_margin_received + expression: + dialects: + - dialect: ANSI_SQL + expression: initial_margin_received + description: IM received. Unsigned; do not sum with posted — subtract. + custom_extensions: + - vendor_name: SEMANTIDO + data: '{"data_type": "DECIMAL"}' + - name: collateral_currency + expression: + dialects: + - dialect: ANSI_SQL + expression: collateral_currency + description: ISO 4217 currency of margin amounts. + custom_extensions: + - vendor_name: SEMANTIDO + data: '{"data_type": "VARCHAR"}' + custom_extensions: + - vendor_name: SEMANTIDO + data: '{"time_dimension": "reporting_date"}' + - name: counterparty + source: counterparty + primary_key: + - lei + description: Legal entity master for derivative counterparties, one row per LEI. + ai_context: + instructions: EMIR counterparty classification drives clearing and reporting + obligations (FC / NFC+ / NFC- under Art. 2 and Art. 10). + synonyms: + - legal entities + - counterparties + fields: + - name: lei + expression: + dialects: + - dialect: ANSI_SQL + expression: lei + description: Legal Entity Identifier (ISO 17442). The sole join key for counterparty + identity everywhere in this schema. + ai_context: + examples: + - 529900T8BM49AURSDO55 + custom_extensions: + - vendor_name: SEMANTIDO + data: '{"data_type": "VARCHAR"}' + - name: legal_name + expression: + dialects: + - dialect: ANSI_SQL + expression: legal_name + description: Registered legal name as per GLEIF record. + custom_extensions: + - vendor_name: SEMANTIDO + data: '{"data_type": "VARCHAR", "privacy_level": "confidential"}' + - name: classification + expression: + dialects: + - dialect: ANSI_SQL + expression: classification + description: 'EMIR counterparty classification: ''FC'' (financial counterparty, + Art. 2(8)), ''NFC+'' (non-financial above clearing threshold, Art. 10), ''NFC-'' + (below). Filter on this before any obligation question.' + ai_context: + examples: + - FC + - NFC+ + custom_extensions: + - vendor_name: SEMANTIDO + data: '{"data_type": "VARCHAR"}' + - name: country + expression: + dialects: + - dialect: ANSI_SQL + expression: country + description: ISO 3166-1 alpha-2 country of incorporation. + custom_extensions: + - vendor_name: SEMANTIDO + data: '{"data_type": "VARCHAR"}' + - name: counterparty_trade_role + source: counterparty_trade_role + primary_key: + - id + description: Role bridge between trades and counterparties. A trade has AT LEAST + two rows here (reporting + other counterparty), often more. + ai_context: + instructions: 'FAN-OUT GUARD: joining trade -> this table -> counterparty multiplies + trade rows by role count; any SUM over trade amounts after this join double-counts. + Filter to a single role (usually ''RPTG'') before aggregating trade-level + amounts.' + fields: + - name: id + expression: + dialects: + - dialect: ANSI_SQL + expression: id + description: 'Column: id' + custom_extensions: + - vendor_name: SEMANTIDO + data: '{"data_type": "INTEGER"}' + - name: uti + expression: + dialects: + - dialect: ANSI_SQL + expression: uti + description: 'Column: uti' + custom_extensions: + - vendor_name: SEMANTIDO + data: '{"data_type": "VARCHAR", "references": "trade.uti"}' + - name: lei + expression: + dialects: + - dialect: ANSI_SQL + expression: lei + description: 'Column: lei' + custom_extensions: + - vendor_name: SEMANTIDO + data: '{"data_type": "VARCHAR", "references": "counterparty.lei"}' + - name: role + expression: + dialects: + - dialect: ANSI_SQL + expression: role + description: '''RPTG'' reporting counterparty, ''OTHR'' other counterparty, + ''BRKR'' broker, ''CLRM'' clearing member. Exactly one RPTG row per UTI.' + ai_context: + examples: + - RPTG + - OTHR + custom_extensions: + - vendor_name: SEMANTIDO + data: '{"data_type": "VARCHAR"}' + - name: submission_log + source: submission_log + primary_key: + - id + description: 'Trade repository submission log. Grain: one row per submission attempt + — a trade may have many.' + ai_context: + instructions: Rejection-rate questions resolve here, per attempt, not per trade, + unless deduplicated. Rejection rate = NACK / all attempts. + fields: + - name: id + expression: + dialects: + - dialect: ANSI_SQL + expression: id + description: 'Column: id' + custom_extensions: + - vendor_name: SEMANTIDO + data: '{"data_type": "INTEGER"}' + - name: uti + expression: + dialects: + - dialect: ANSI_SQL + expression: uti + description: 'Column: uti' + custom_extensions: + - vendor_name: SEMANTIDO + data: '{"data_type": "VARCHAR", "references": "trade.uti"}' + - name: submission_timestamp + expression: + dialects: + - dialect: ANSI_SQL + expression: submission_timestamp + dimension: + is_time: true + description: 'Column: submission_timestamp' + custom_extensions: + - vendor_name: SEMANTIDO + data: '{"data_type": "TIMESTAMP", "time_grain": "second"}' + - name: status + expression: + dialects: + - dialect: ANSI_SQL + expression: status + description: '''ACKD'' accepted, ''NACK'' rejected, ''PEND'' pending.' + ai_context: + examples: + - ACKD + - NACK + custom_extensions: + - vendor_name: SEMANTIDO + data: '{"data_type": "VARCHAR"}' + - name: nack_reason + expression: + dialects: + - dialect: ANSI_SQL + expression: nack_reason + description: TR rejection reason text; NULL unless NACK. + custom_extensions: + - vendor_name: SEMANTIDO + data: '{"data_type": "VARCHAR"}' + custom_extensions: + - vendor_name: SEMANTIDO + data: '{"time_dimension": "submission_timestamp"}' + - name: trade + source: trade + primary_key: + - uti + description: Reportable OTC derivative trades, one row per UTI. Immutable birth + record of the trade; evolving economics live in trade_state. + ai_context: + instructions: 'AMBIGUITY GUARD: notional_amount is contract size, NOT current + value and NOT exposure. ''How big'' -> notional; ''worth today'' -> trade_state.valuation_amount; + ''at risk'' -> collateral_report.' + synonyms: + - trades + - derivatives + - positions + fields: + - name: uti + expression: + dialects: + - dialect: ANSI_SQL + expression: uti + description: Unique Trade Identifier (Art. 9 REFIT). Canonical trade key. + custom_extensions: + - vendor_name: SEMANTIDO + data: '{"data_type": "VARCHAR"}' + - name: asset_class + expression: + dialects: + - dialect: ANSI_SQL + expression: asset_class + description: 'EMIR asset class: ''IR'' rates, ''CR'' credit, ''EQ'' equity, + ''FX'' FX, ''CO'' commodity.' + ai_context: + examples: + - IR + - FX + custom_extensions: + - vendor_name: SEMANTIDO + data: '{"data_type": "VARCHAR"}' + - name: notional_amount + expression: + dialects: + - dialect: ANSI_SQL + expression: notional_amount + description: Trade notional in notional_currency. Contract size, not value. + Never sum across currencies without conversion. + custom_extensions: + - vendor_name: SEMANTIDO + data: '{"data_type": "DECIMAL", "privacy_level": "restricted"}' + - name: notional_currency + expression: + dialects: + - dialect: ANSI_SQL + expression: notional_currency + description: ISO 4217 currency of notional_amount. + ai_context: + examples: + - EUR + - USD + custom_extensions: + - vendor_name: SEMANTIDO + data: '{"data_type": "VARCHAR"}' + - name: execution_timestamp + expression: + dialects: + - dialect: ANSI_SQL + expression: execution_timestamp + dimension: + is_time: true + description: UTC execution time (Art. 9 REFIT). Primary time axis. + custom_extensions: + - vendor_name: SEMANTIDO + data: '{"data_type": "TIMESTAMP", "time_grain": "second"}' + custom_extensions: + - vendor_name: SEMANTIDO + data: '{"time_dimension": "execution_timestamp"}' + - name: trade_state + source: trade_state + primary_key: + - id + description: Daily trade-state reports (EMIR REFIT is state-based), one row per + UTI per reporting_date. The latest state per UTI is the current view. + ai_context: + instructions: 'SIGN CONVENTION: valuation_amount is signed from the REPORTING + counterparty''s perspective. Aggregating across counterparties without flipping + signs by role is meaningless.' + fields: + - name: id + expression: + dialects: + - dialect: ANSI_SQL + expression: id + description: 'Column: id' + custom_extensions: + - vendor_name: SEMANTIDO + data: '{"data_type": "INTEGER"}' + - name: uti + expression: + dialects: + - dialect: ANSI_SQL + expression: uti + description: 'Column: uti' + custom_extensions: + - vendor_name: SEMANTIDO + data: '{"data_type": "VARCHAR", "references": "trade.uti"}' + - name: reporting_date + expression: + dialects: + - dialect: ANSI_SQL + expression: reporting_date + dimension: + is_time: true + description: State date; one state per trade per day. + custom_extensions: + - vendor_name: SEMANTIDO + data: '{"data_type": "DATE", "time_grain": "day"}' + - name: valuation_amount + expression: + dialects: + - dialect: ANSI_SQL + expression: valuation_amount + description: 'Mark-to-market value (Art. 11(2)), signed from the reporting counterparty''s + perspective. Value, not size: do not confuse with notional.' + custom_extensions: + - vendor_name: SEMANTIDO + data: '{"data_type": "DECIMAL", "privacy_level": "restricted"}' + - name: contract_status + expression: + dialects: + - dialect: ANSI_SQL + expression: contract_status + description: '''OUTS'' outstanding, ''TERM'' terminated, ''MATU'' matured, ''ERRO'' + error. Exclude non-OUTS from exposure aggregates unless asked otherwise.' + ai_context: + examples: + - OUTS + - TERM + custom_extensions: + - vendor_name: SEMANTIDO + data: '{"data_type": "VARCHAR"}' + custom_extensions: + - vendor_name: SEMANTIDO + data: '{"sql_filters": ["reporting_date = (SELECT MAX(ts2.reporting_date) FROM + trade_state ts2 WHERE ts2.uti = trade_state.uti)"], "time_dimension": "reporting_date"}' + relationships: + - name: counterparty_to_counterparty_trade_role + from: counterparty + to: counterparty_trade_role + from_columns: + - lei + to_columns: + - lei + ai_context: + instructions: Roles this entity plays on trades. + custom_extensions: + - vendor_name: SEMANTIDO + data: '{"relationship_type": "one-to-many"}' + - name: counterparty_trade_role_to_trade + from: counterparty_trade_role + to: trade + from_columns: + - uti + to_columns: + - uti + ai_context: + instructions: Trade this role row belongs to. + custom_extensions: + - vendor_name: SEMANTIDO + data: '{"relationship_type": "many-to-one"}' + - name: counterparty_trade_role_to_counterparty + from: counterparty_trade_role + to: counterparty + from_columns: + - lei + to_columns: + - lei + ai_context: + instructions: Entity playing this role. + custom_extensions: + - vendor_name: SEMANTIDO + data: '{"relationship_type": "many-to-one"}' + - name: trade_to_trade_state + from: trade + to: trade_state + from_columns: + - uti + to_columns: + - uti + ai_context: + instructions: Daily state reports for this trade. + custom_extensions: + - vendor_name: SEMANTIDO + data: '{"relationship_type": "one-to-many"}' + - name: trade_to_counterparty_trade_role + from: trade + to: counterparty_trade_role + from_columns: + - uti + to_columns: + - uti + ai_context: + instructions: Counterparty roles attached to this trade. + custom_extensions: + - vendor_name: SEMANTIDO + data: '{"relationship_type": "one-to-many"}' + - name: trade_state_to_trade + from: trade_state + to: trade + from_columns: + - uti + to_columns: + - uti + ai_context: + instructions: The trade this state report belongs to. + custom_extensions: + - vendor_name: SEMANTIDO + data: '{"relationship_type": "many-to-one"}' From aecd2119ead1cefded79c522994e7c692a99f822 Mon Sep 17 00:00:00 2001 From: Dragos Crintea Date: Sat, 18 Jul 2026 23:26:43 +0100 Subject: [PATCH 4/6] semantido <-> ossie converter --- examples/semantido_to_osi_emir_reporting.yaml | 464 ------------------ 1 file changed, 464 deletions(-) delete mode 100644 examples/semantido_to_osi_emir_reporting.yaml diff --git a/examples/semantido_to_osi_emir_reporting.yaml b/examples/semantido_to_osi_emir_reporting.yaml deleted file mode 100644 index 582334b3..00000000 --- a/examples/semantido_to_osi_emir_reporting.yaml +++ /dev/null @@ -1,464 +0,0 @@ -version: 0.2.0.dev0 -semantic_model: -- name: emir_reporting - datasets: - - name: collateral_report - source: collateral_report - primary_key: - - id - description: Daily collateral/margin per UTI (Art. 11(3)). - ai_context: - instructions: 'SIGN CONVENTION (different from trade_state!): amounts are UNSIGNED; - direction is carried by which column they sit in. Net collateral = posted - - received, computed, never read from a column.' - fields: - - name: id - expression: - dialects: - - dialect: ANSI_SQL - expression: id - description: 'Column: id' - custom_extensions: - - vendor_name: SEMANTIDO - data: '{"data_type": "INTEGER"}' - - name: uti - expression: - dialects: - - dialect: ANSI_SQL - expression: uti - description: 'Column: uti' - custom_extensions: - - vendor_name: SEMANTIDO - data: '{"data_type": "VARCHAR", "references": "trade.uti"}' - - name: reporting_date - expression: - dialects: - - dialect: ANSI_SQL - expression: reporting_date - dimension: - is_time: true - description: 'Column: reporting_date' - custom_extensions: - - vendor_name: SEMANTIDO - data: '{"data_type": "DATE", "time_grain": "day"}' - - name: initial_margin_posted - expression: - dialects: - - dialect: ANSI_SQL - expression: initial_margin_posted - description: IM posted BY the reporting counterparty. Unsigned. - custom_extensions: - - vendor_name: SEMANTIDO - data: '{"data_type": "DECIMAL"}' - - name: initial_margin_received - expression: - dialects: - - dialect: ANSI_SQL - expression: initial_margin_received - description: IM received. Unsigned; do not sum with posted — subtract. - custom_extensions: - - vendor_name: SEMANTIDO - data: '{"data_type": "DECIMAL"}' - - name: collateral_currency - expression: - dialects: - - dialect: ANSI_SQL - expression: collateral_currency - description: ISO 4217 currency of margin amounts. - custom_extensions: - - vendor_name: SEMANTIDO - data: '{"data_type": "VARCHAR"}' - custom_extensions: - - vendor_name: SEMANTIDO - data: '{"time_dimension": "reporting_date"}' - - name: counterparty - source: counterparty - primary_key: - - lei - description: Legal entity master for derivative counterparties, one row per LEI. - ai_context: - instructions: EMIR counterparty classification drives clearing and reporting - obligations (FC / NFC+ / NFC- under Art. 2 and Art. 10). - synonyms: - - legal entities - - counterparties - fields: - - name: lei - expression: - dialects: - - dialect: ANSI_SQL - expression: lei - description: Legal Entity Identifier (ISO 17442). The sole join key for counterparty - identity everywhere in this schema. - ai_context: - examples: - - 529900T8BM49AURSDO55 - custom_extensions: - - vendor_name: SEMANTIDO - data: '{"data_type": "VARCHAR"}' - - name: legal_name - expression: - dialects: - - dialect: ANSI_SQL - expression: legal_name - description: Registered legal name as per GLEIF record. - custom_extensions: - - vendor_name: SEMANTIDO - data: '{"data_type": "VARCHAR", "privacy_level": "confidential"}' - - name: classification - expression: - dialects: - - dialect: ANSI_SQL - expression: classification - description: 'EMIR counterparty classification: ''FC'' (financial counterparty, - Art. 2(8)), ''NFC+'' (non-financial above clearing threshold, Art. 10), ''NFC-'' - (below). Filter on this before any obligation question.' - ai_context: - examples: - - FC - - NFC+ - custom_extensions: - - vendor_name: SEMANTIDO - data: '{"data_type": "VARCHAR"}' - - name: country - expression: - dialects: - - dialect: ANSI_SQL - expression: country - description: ISO 3166-1 alpha-2 country of incorporation. - custom_extensions: - - vendor_name: SEMANTIDO - data: '{"data_type": "VARCHAR"}' - - name: counterparty_trade_role - source: counterparty_trade_role - primary_key: - - id - description: Role bridge between trades and counterparties. A trade has AT LEAST - two rows here (reporting + other counterparty), often more. - ai_context: - instructions: 'FAN-OUT GUARD: joining trade -> this table -> counterparty multiplies - trade rows by role count; any SUM over trade amounts after this join double-counts. - Filter to a single role (usually ''RPTG'') before aggregating trade-level - amounts.' - fields: - - name: id - expression: - dialects: - - dialect: ANSI_SQL - expression: id - description: 'Column: id' - custom_extensions: - - vendor_name: SEMANTIDO - data: '{"data_type": "INTEGER"}' - - name: uti - expression: - dialects: - - dialect: ANSI_SQL - expression: uti - description: 'Column: uti' - custom_extensions: - - vendor_name: SEMANTIDO - data: '{"data_type": "VARCHAR", "references": "trade.uti"}' - - name: lei - expression: - dialects: - - dialect: ANSI_SQL - expression: lei - description: 'Column: lei' - custom_extensions: - - vendor_name: SEMANTIDO - data: '{"data_type": "VARCHAR", "references": "counterparty.lei"}' - - name: role - expression: - dialects: - - dialect: ANSI_SQL - expression: role - description: '''RPTG'' reporting counterparty, ''OTHR'' other counterparty, - ''BRKR'' broker, ''CLRM'' clearing member. Exactly one RPTG row per UTI.' - ai_context: - examples: - - RPTG - - OTHR - custom_extensions: - - vendor_name: SEMANTIDO - data: '{"data_type": "VARCHAR"}' - - name: submission_log - source: submission_log - primary_key: - - id - description: 'Trade repository submission log. Grain: one row per submission attempt - — a trade may have many.' - ai_context: - instructions: Rejection-rate questions resolve here, per attempt, not per trade, - unless deduplicated. Rejection rate = NACK / all attempts. - fields: - - name: id - expression: - dialects: - - dialect: ANSI_SQL - expression: id - description: 'Column: id' - custom_extensions: - - vendor_name: SEMANTIDO - data: '{"data_type": "INTEGER"}' - - name: uti - expression: - dialects: - - dialect: ANSI_SQL - expression: uti - description: 'Column: uti' - custom_extensions: - - vendor_name: SEMANTIDO - data: '{"data_type": "VARCHAR", "references": "trade.uti"}' - - name: submission_timestamp - expression: - dialects: - - dialect: ANSI_SQL - expression: submission_timestamp - dimension: - is_time: true - description: 'Column: submission_timestamp' - custom_extensions: - - vendor_name: SEMANTIDO - data: '{"data_type": "TIMESTAMP", "time_grain": "second"}' - - name: status - expression: - dialects: - - dialect: ANSI_SQL - expression: status - description: '''ACKD'' accepted, ''NACK'' rejected, ''PEND'' pending.' - ai_context: - examples: - - ACKD - - NACK - custom_extensions: - - vendor_name: SEMANTIDO - data: '{"data_type": "VARCHAR"}' - - name: nack_reason - expression: - dialects: - - dialect: ANSI_SQL - expression: nack_reason - description: TR rejection reason text; NULL unless NACK. - custom_extensions: - - vendor_name: SEMANTIDO - data: '{"data_type": "VARCHAR"}' - custom_extensions: - - vendor_name: SEMANTIDO - data: '{"time_dimension": "submission_timestamp"}' - - name: trade - source: trade - primary_key: - - uti - description: Reportable OTC derivative trades, one row per UTI. Immutable birth - record of the trade; evolving economics live in trade_state. - ai_context: - instructions: 'AMBIGUITY GUARD: notional_amount is contract size, NOT current - value and NOT exposure. ''How big'' -> notional; ''worth today'' -> trade_state.valuation_amount; - ''at risk'' -> collateral_report.' - synonyms: - - trades - - derivatives - - positions - fields: - - name: uti - expression: - dialects: - - dialect: ANSI_SQL - expression: uti - description: Unique Trade Identifier (Art. 9 REFIT). Canonical trade key. - custom_extensions: - - vendor_name: SEMANTIDO - data: '{"data_type": "VARCHAR"}' - - name: asset_class - expression: - dialects: - - dialect: ANSI_SQL - expression: asset_class - description: 'EMIR asset class: ''IR'' rates, ''CR'' credit, ''EQ'' equity, - ''FX'' FX, ''CO'' commodity.' - ai_context: - examples: - - IR - - FX - custom_extensions: - - vendor_name: SEMANTIDO - data: '{"data_type": "VARCHAR"}' - - name: notional_amount - expression: - dialects: - - dialect: ANSI_SQL - expression: notional_amount - description: Trade notional in notional_currency. Contract size, not value. - Never sum across currencies without conversion. - custom_extensions: - - vendor_name: SEMANTIDO - data: '{"data_type": "DECIMAL", "privacy_level": "restricted"}' - - name: notional_currency - expression: - dialects: - - dialect: ANSI_SQL - expression: notional_currency - description: ISO 4217 currency of notional_amount. - ai_context: - examples: - - EUR - - USD - custom_extensions: - - vendor_name: SEMANTIDO - data: '{"data_type": "VARCHAR"}' - - name: execution_timestamp - expression: - dialects: - - dialect: ANSI_SQL - expression: execution_timestamp - dimension: - is_time: true - description: UTC execution time (Art. 9 REFIT). Primary time axis. - custom_extensions: - - vendor_name: SEMANTIDO - data: '{"data_type": "TIMESTAMP", "time_grain": "second"}' - custom_extensions: - - vendor_name: SEMANTIDO - data: '{"time_dimension": "execution_timestamp"}' - - name: trade_state - source: trade_state - primary_key: - - id - description: Daily trade-state reports (EMIR REFIT is state-based), one row per - UTI per reporting_date. The latest state per UTI is the current view. - ai_context: - instructions: 'SIGN CONVENTION: valuation_amount is signed from the REPORTING - counterparty''s perspective. Aggregating across counterparties without flipping - signs by role is meaningless.' - fields: - - name: id - expression: - dialects: - - dialect: ANSI_SQL - expression: id - description: 'Column: id' - custom_extensions: - - vendor_name: SEMANTIDO - data: '{"data_type": "INTEGER"}' - - name: uti - expression: - dialects: - - dialect: ANSI_SQL - expression: uti - description: 'Column: uti' - custom_extensions: - - vendor_name: SEMANTIDO - data: '{"data_type": "VARCHAR", "references": "trade.uti"}' - - name: reporting_date - expression: - dialects: - - dialect: ANSI_SQL - expression: reporting_date - dimension: - is_time: true - description: State date; one state per trade per day. - custom_extensions: - - vendor_name: SEMANTIDO - data: '{"data_type": "DATE", "time_grain": "day"}' - - name: valuation_amount - expression: - dialects: - - dialect: ANSI_SQL - expression: valuation_amount - description: 'Mark-to-market value (Art. 11(2)), signed from the reporting counterparty''s - perspective. Value, not size: do not confuse with notional.' - custom_extensions: - - vendor_name: SEMANTIDO - data: '{"data_type": "DECIMAL", "privacy_level": "restricted"}' - - name: contract_status - expression: - dialects: - - dialect: ANSI_SQL - expression: contract_status - description: '''OUTS'' outstanding, ''TERM'' terminated, ''MATU'' matured, ''ERRO'' - error. Exclude non-OUTS from exposure aggregates unless asked otherwise.' - ai_context: - examples: - - OUTS - - TERM - custom_extensions: - - vendor_name: SEMANTIDO - data: '{"data_type": "VARCHAR"}' - custom_extensions: - - vendor_name: SEMANTIDO - data: '{"sql_filters": ["reporting_date = (SELECT MAX(ts2.reporting_date) FROM - trade_state ts2 WHERE ts2.uti = trade_state.uti)"], "time_dimension": "reporting_date"}' - relationships: - - name: counterparty_to_counterparty_trade_role - from: counterparty - to: counterparty_trade_role - from_columns: - - lei - to_columns: - - lei - ai_context: - instructions: Roles this entity plays on trades. - custom_extensions: - - vendor_name: SEMANTIDO - data: '{"relationship_type": "one-to-many"}' - - name: counterparty_trade_role_to_trade - from: counterparty_trade_role - to: trade - from_columns: - - uti - to_columns: - - uti - ai_context: - instructions: Trade this role row belongs to. - custom_extensions: - - vendor_name: SEMANTIDO - data: '{"relationship_type": "many-to-one"}' - - name: counterparty_trade_role_to_counterparty - from: counterparty_trade_role - to: counterparty - from_columns: - - lei - to_columns: - - lei - ai_context: - instructions: Entity playing this role. - custom_extensions: - - vendor_name: SEMANTIDO - data: '{"relationship_type": "many-to-one"}' - - name: trade_to_trade_state - from: trade - to: trade_state - from_columns: - - uti - to_columns: - - uti - ai_context: - instructions: Daily state reports for this trade. - custom_extensions: - - vendor_name: SEMANTIDO - data: '{"relationship_type": "one-to-many"}' - - name: trade_to_counterparty_trade_role - from: trade - to: counterparty_trade_role - from_columns: - - uti - to_columns: - - uti - ai_context: - instructions: Counterparty roles attached to this trade. - custom_extensions: - - vendor_name: SEMANTIDO - data: '{"relationship_type": "one-to-many"}' - - name: trade_state_to_trade - from: trade_state - to: trade - from_columns: - - uti - to_columns: - - uti - ai_context: - instructions: The trade this state report belongs to. - custom_extensions: - - vendor_name: SEMANTIDO - data: '{"relationship_type": "many-to-one"}' From 17992f9b41011681040dd746b0f3b4739657145f Mon Sep 17 00:00:00 2001 From: Dragos Crintea Date: Tue, 21 Jul 2026 19:04:33 +0100 Subject: [PATCH 5/6] resolved raised PR request comments --- converters/semantido/pyproject.toml | 3 +- .../semantido/src/ossie_semantido/__init__.py | 6 +- .../semantido/src/ossie_semantido/cli.py | 54 ++++++++++++----- .../src/ossie_semantido/constants.py | 20 +++++++ .../src/ossie_semantido/osi_to_semantido.py | 25 +++++--- .../src/ossie_semantido/semantido_to_osi.py | 58 ++++++++++++++----- 6 files changed, 129 insertions(+), 37 deletions(-) create mode 100644 converters/semantido/src/ossie_semantido/constants.py diff --git a/converters/semantido/pyproject.toml b/converters/semantido/pyproject.toml index 89f5d54e..a2d27d39 100644 --- a/converters/semantido/pyproject.toml +++ b/converters/semantido/pyproject.toml @@ -30,7 +30,7 @@ classifiers = [ ] dependencies = [ "apache-ossie>=0.2.0.dev0", - "semantido>=0.4.0", + "semantido>=0.4.1", "SQLAlchemy>=2.0", "PyYAML>=6.0", ] @@ -47,6 +47,7 @@ packages = ["src/ossie_semantido"] [tool.uv] dev-dependencies = [ "pytest>=8.0", + "ruff==0.15.22" ] [tool.uv.sources] diff --git a/converters/semantido/src/ossie_semantido/__init__.py b/converters/semantido/src/ossie_semantido/__init__.py index 8a4f9377..1014e6f9 100644 --- a/converters/semantido/src/ossie_semantido/__init__.py +++ b/converters/semantido/src/ossie_semantido/__init__.py @@ -17,7 +17,11 @@ """semantido <> Apache Ossie converter.""" -from ossie_semantido.converter_issues import ConverterIssue, ConverterIssueType, ConverterResult +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 diff --git a/converters/semantido/src/ossie_semantido/cli.py b/converters/semantido/src/ossie_semantido/cli.py index 7e26df68..138cbd26 100644 --- a/converters/semantido/src/ossie_semantido/cli.py +++ b/converters/semantido/src/ossie_semantido/cli.py @@ -30,7 +30,9 @@ def _report_issues(issues) -> None: for issue in issues: - print(f"warning: {issue.issue_type.value}: {issue.element_name}", file=sys.stderr) + print( + f"warning: {issue.issue_type.value}: {issue.element_name}", file=sys.stderr + ) def _cmd_semantido_to_osi(args: argparse.Namespace) -> None: @@ -60,23 +62,49 @@ def main() -> None: 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.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.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() diff --git a/converters/semantido/src/ossie_semantido/constants.py b/converters/semantido/src/ossie_semantido/constants.py new file mode 100644 index 00000000..5e07cbb1 --- /dev/null +++ b/converters/semantido/src/ossie_semantido/constants.py @@ -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" diff --git a/converters/semantido/src/ossie_semantido/osi_to_semantido.py b/converters/semantido/src/ossie_semantido/osi_to_semantido.py index 91a4aa2c..8aa9eb0c 100644 --- a/converters/semantido/src/ossie_semantido/osi_to_semantido.py +++ b/converters/semantido/src/ossie_semantido/osi_to_semantido.py @@ -29,9 +29,12 @@ from ossie import OSIDataset, OSIDocument, OSIField -from ossie_semantido.converter_issues import ConverterIssue, ConverterIssueType, ConverterResult - -VENDOR_NAME = "SEMANTIDO" +from ossie_semantido.constants import VENDOR_NAME +from ossie_semantido.converter_issues import ( + ConverterIssue, + ConverterIssueType, + ConverterResult, +) _TYPE_MAP = { "VARCHAR": "String", @@ -67,7 +70,9 @@ def _sqlalchemy_type(field: OSIField, issues: List[ConverterIssue]) -> str: return _TYPE_MAP[base] if base: issues.append( - ConverterIssue(ConverterIssueType.UNMAPPED_DATA_TYPE, f"{field.name}:{base}") + ConverterIssue( + ConverterIssueType.UNMAPPED_DATA_TYPE, f"{field.name}:{base}" + ) ) return "String" @@ -117,7 +122,9 @@ def _emit_dataset(dataset: OSIDataset, issues: List[ConverterIssue]) -> str: level = field_payload["privacy_level"].upper() lines.append(f" {field.name}_privacy_level = PrivacyLevel.{level}") if field_ctx and field_ctx.examples: - lines.append(f" {field.name}_sample_values = {[str(e) for e in field_ctx.examples]!r}") + lines.append( + f" {field.name}_sample_values = {[str(e) for e in field_ctx.examples]!r}" + ) if field_payload.get("time_grain"): grain = field_payload["time_grain"].upper() lines.append(f" {field.name}_time_grain = TimeGrain.{grain}") @@ -131,13 +138,17 @@ def osi_to_semantido_source(document: OSIDocument) -> ConverterResult[str]: for model in document.semantic_model: for metric in model.metrics or []: - issues.append(ConverterIssue(ConverterIssueType.METRIC_DROPPED, metric.name)) + issues.append( + ConverterIssue(ConverterIssueType.METRIC_DROPPED, metric.name) + ) for dataset in model.datasets: blocks.append(_emit_dataset(dataset, issues)) todo_block = "" if issues: - todo_lines = "\n".join(f"# {i.issue_type.value}: {i.element_name}" for i in issues) + todo_lines = "\n".join( + f"# {i.issue_type.value}: {i.element_name}" for i in issues + ) todo_block = ( "# TODO(ossie-semantido): the following Ossie constructs have no\n" "# semantido equivalent yet and were not converted:\n" + todo_lines + "\n\n" diff --git a/converters/semantido/src/ossie_semantido/semantido_to_osi.py b/converters/semantido/src/ossie_semantido/semantido_to_osi.py index 74743b85..9cf55689 100644 --- a/converters/semantido/src/ossie_semantido/semantido_to_osi.py +++ b/converters/semantido/src/ossie_semantido/semantido_to_osi.py @@ -17,7 +17,7 @@ """semantido SemanticLayer -> Apache Ossie document conversion. -Builds the Ossie model through the typed ``apache-ossie`` objects so the +Builds the Ossie model through the typed ``apache-ossie`` objects, so the output is schema-conformant by construction. Governance metadata that has no Ossie core field (privacy levels, SQL filters, time grain, the primary time axis) is preserved losslessly in ``custom_extensions`` under the @@ -42,16 +42,22 @@ OSIRelationship, OSISemanticModel, ) -from semantido.generators.semantic_layer import Column, Relationship, SemanticLayer, Table - -from ossie_semantido.converter_issues import ConverterIssue, ConverterIssueType, ConverterResult - -VENDOR_NAME = "SEMANTIDO" +from semantido.generators.semantic_layer import ( + Column, + Relationship, + SemanticLayer, + Table, +) -_JOIN_RE = re.compile( - r"^\s*(\w+)\.(\w+)\s*=\s*(\w+)\.(\w+)\s*$" +from ossie_semantido.constants import VENDOR_NAME +from ossie_semantido.converter_issues import ( + ConverterIssue, + ConverterIssueType, + ConverterResult, ) +_JOIN_RE = re.compile(r"^\s*(\w+)\.(\w+)\s*=\s*(\w+)\.(\w+)\s*$") + def _extension(payload: dict) -> OSICustomExtension: """Wrap semantido-specific metadata as a spec-conformant custom extension.""" @@ -63,7 +69,9 @@ def _extension(payload: dict) -> OSICustomExtension: def _ansi(expression: str) -> OSIExpression: return OSIExpression( - dialects=[OSIDialectExpression(dialect=OSIDialect.ANSI_SQL, expression=expression)] + dialects=[ + OSIDialectExpression(dialect=OSIDialect.ANSI_SQL, expression=expression) + ] ) @@ -92,12 +100,16 @@ def _column_to_field(column: Column) -> OSIField: dimension=OSIDimension(is_time=True) if column.is_time_dimension else None, description=column.description or None, ai_context=OSIAIContextObject(**ai_kwargs) if ai_kwargs else None, - custom_extensions=[_extension(extension_payload)] if extension_payload else None, + custom_extensions=[_extension(extension_payload)] + if extension_payload + else None, ) def _table_to_dataset(table: Table) -> OSIDataset: - instructions_parts = [p for p in (table.business_context, table.application_context) if p] + instructions_parts = [ + p for p in (table.business_context, table.application_context) if p + ] ai_kwargs = {} if instructions_parts: @@ -118,7 +130,9 @@ def _table_to_dataset(table: Table) -> OSIDataset: description=table.description or None, ai_context=OSIAIContextObject(**ai_kwargs) if ai_kwargs else None, fields=[_column_to_field(c) for c in table.columns] or None, - custom_extensions=[_extension(extension_payload)] if extension_payload else None, + custom_extensions=[_extension(extension_payload)] + if extension_payload + else None, ) @@ -140,8 +154,18 @@ def _relationship_to_osi( # which side of the equality each table appeared on. if left_table == rel.from_table: from_columns, to_columns = [left_col], [right_col] - else: + elif right_table == rel.from_table: from_columns, to_columns = [right_col], [left_col] + else: + # Neither side of the join references rel.from_table (e.g., aliases) + # Treat this as unparseable rather than silently swapping columns. + issues.append( + ConverterIssue( + issue_type=ConverterIssueType.RELATIONSHIP_JOIN_UNPARSED, + element_name=f"{rel.from_table}->{rel.to_table}", + ) + ) + return None ai_kwargs = {} if rel.description: @@ -156,7 +180,9 @@ def _relationship_to_osi( "to_columns": to_columns, "ai_context": ai_kwargs or None, "custom_extensions": [ - _extension({"relationship_type": rel.relationship_type.value}).model_dump() + _extension( + {"relationship_type": rel.relationship_type.value} + ).model_dump() ], } ) @@ -178,7 +204,9 @@ def semantic_layer_to_osi( model_extensions = None if layer.application_glossary: - model_extensions = [_extension({"application_glossary": layer.application_glossary})] + model_extensions = [ + _extension({"application_glossary": layer.application_glossary}) + ] model = OSISemanticModel( name=model_name, From b91ed707157ac5621d0b1dffd7018edd72f5f5c9 Mon Sep 17 00:00:00 2001 From: Dragos Crintea Date: Sun, 2 Aug 2026 18:15:24 +0100 Subject: [PATCH 6/6] update semantido reference to Apache Ossie and bump to v0.5.2 --- converters/semantido/README.md | 7 + converters/semantido/pyproject.toml | 14 +- .../semantido/src/ossie_semantido/__init__.py | 8 +- .../semantido/src/ossie_semantido/cli.py | 36 +- .../src/ossie_semantido/converter_issues.py | 1 + .../semantido/src/ossie_semantido/loaders.py | 9 +- .../src/ossie_semantido/osi_to_semantido.py | 164 ------- .../src/ossie_semantido/ossie_to_semantido.py | 344 +++++++++++++ ...antido_to_osi.py => semantido_to_ossie.py} | 36 +- .../emir_reporting/models/emir_reporting.py | 12 +- ...emantido.py => test_ossie_to_semantido.py} | 17 +- ...o_to_osi.py => test_semantido_to_ossie.py} | 18 +- .../semantido/tests/test_v05_roundtrip.py | 140 ++++++ converters/semantido/uv.lock | 458 ++++++++++++++++++ 14 files changed, 1044 insertions(+), 220 deletions(-) delete mode 100644 converters/semantido/src/ossie_semantido/osi_to_semantido.py create mode 100644 converters/semantido/src/ossie_semantido/ossie_to_semantido.py rename converters/semantido/src/ossie_semantido/{semantido_to_osi.py => semantido_to_ossie.py} (81%) rename converters/semantido/tests/{test_osi_to_semantido.py => test_ossie_to_semantido.py} (68%) rename converters/semantido/tests/{test_semantido_to_osi.py => test_semantido_to_ossie.py} (82%) create mode 100644 converters/semantido/tests/test_v05_roundtrip.py create mode 100644 converters/semantido/uv.lock diff --git a/converters/semantido/README.md b/converters/semantido/README.md index fb11b97b..6a828533 100644 --- a/converters/semantido/README.md +++ b/converters/semantido/README.md @@ -29,6 +29,13 @@ 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. +Tracks the **semantido ≥ 0.5.2** surface: concept bindings (`concept=` / +`_concept`), the concept registry — definitions, relations, +external mappings, and grain — carried as a model-level custom extension, +and `unique_keys`. A module-level `CONCEPT_REGISTRY` in the loaded models +module is passed to `sync_semantic_layer()` automatically; the reverse +direction regenerates it as a `build_registry()` function. + ## Usage ```bash diff --git a/converters/semantido/pyproject.toml b/converters/semantido/pyproject.toml index a2d27d39..d00545f3 100644 --- a/converters/semantido/pyproject.toml +++ b/converters/semantido/pyproject.toml @@ -30,11 +30,17 @@ classifiers = [ ] dependencies = [ "apache-ossie>=0.2.0.dev0", - "semantido>=0.4.1", + "semantido>=0.5.2", "SQLAlchemy>=2.0", "PyYAML>=6.0", ] +[project.optional-dependencies] +dev = [ + "pytest>=8.0", + "ruff==0.15.22" +] + [project.license] text = "Apache-2.0" @@ -44,11 +50,5 @@ 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 } diff --git a/converters/semantido/src/ossie_semantido/__init__.py b/converters/semantido/src/ossie_semantido/__init__.py index 1014e6f9..bb0d3ad1 100644 --- a/converters/semantido/src/ossie_semantido/__init__.py +++ b/converters/semantido/src/ossie_semantido/__init__.py @@ -23,14 +23,14 @@ 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 +from ossie_semantido.ossie_to_semantido import ossie_to_semantido_source +from ossie_semantido.semantido_to_ossie import semantic_layer_to_ossie __all__ = [ "ConverterIssue", "ConverterIssueType", "ConverterResult", "load_from_module", - "osi_to_semantido_source", - "semantic_layer_to_osi", + "ossie_to_semantido_source", + "semantic_layer_to_ossie", ] diff --git a/converters/semantido/src/ossie_semantido/cli.py b/converters/semantido/src/ossie_semantido/cli.py index 138cbd26..85a67fed 100644 --- a/converters/semantido/src/ossie_semantido/cli.py +++ b/converters/semantido/src/ossie_semantido/cli.py @@ -24,8 +24,8 @@ 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 +from ossie_semantido.ossie_to_semantido import ossie_to_semantido_source +from ossie_semantido.semantido_to_ossie import semantic_layer_to_ossie def _report_issues(issues) -> None: @@ -35,18 +35,18 @@ def _report_issues(issues) -> None: ) -def _cmd_semantido_to_osi(args: argparse.Namespace) -> None: +def _cmd_semantido_to_ossie(args: argparse.Namespace) -> None: layer = load_from_module(args.module, sys_path=args.path) - result = semantic_layer_to_osi(layer, model_name=args.name) + result = semantic_layer_to_ossie(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: +def _cmd_ossie_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) + result = ossie_to_semantido_source(document) _report_issues(result.issues) with open(args.output, "w", encoding="utf-8") as handle: handle.write(result.output) @@ -59,53 +59,53 @@ def main() -> None: ) subparsers = parser.add_subparsers(dest="command", required=True) - to_osi = subparsers.add_parser( - "semantido-to-osi", help="Convert decorated SQLAlchemy models → Ossie YAML" + to_ossie = subparsers.add_parser( + "semantido-to-ossie", help="Convert decorated SQLAlchemy models → Ossie YAML" ) - to_osi.add_argument( + to_ossie.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( + to_ossie.add_argument( "-p", "--path", default=None, metavar="DIR", help="Directory prepended to sys.path before import", ) - to_osi.add_argument( + to_ossie.add_argument( "-n", "--name", required=True, metavar="NAME", help="Name for the Ossie semantic_model", ) - to_osi.add_argument( + to_ossie.add_argument( "-o", "--output", required=True, metavar="FILE", help="Path for output Ossie YAML", ) - to_osi.set_defaults(func=_cmd_semantido_to_osi) + to_ossie.set_defaults(func=_cmd_semantido_to_ossie) - from_osi = subparsers.add_parser( - "osi-to-semantido", help="Convert Ossie YAML → generated semantido model code" + from_ossie = subparsers.add_parser( + "ossie-to-semantido", help="Convert Ossie YAML → generated semantido model code" ) - from_osi.add_argument( + from_ossie.add_argument( "-i", "--input", required=True, metavar="FILE", help="Path to Ossie YAML" ) - from_osi.add_argument( + from_ossie.add_argument( "-o", "--output", required=True, metavar="FILE", help="Path for generated Python module", ) - from_osi.set_defaults(func=_cmd_osi_to_semantido) + from_ossie.set_defaults(func=_cmd_ossie_to_semantido) args = parser.parse_args() args.func(args) diff --git a/converters/semantido/src/ossie_semantido/converter_issues.py b/converters/semantido/src/ossie_semantido/converter_issues.py index e35fbfb2..45f70b94 100644 --- a/converters/semantido/src/ossie_semantido/converter_issues.py +++ b/converters/semantido/src/ossie_semantido/converter_issues.py @@ -30,6 +30,7 @@ class ConverterIssueType(Enum): NON_ANSI_EXPRESSION_DROPPED = "NON_ANSI_EXPRESSION_DROPPED" COMPOSITE_PRIMARY_KEY_TRUNCATED = "COMPOSITE_PRIMARY_KEY_TRUNCATED" UNMAPPED_DATA_TYPE = "UNMAPPED_DATA_TYPE" + REGISTRY_RELATION_UNRESOLVED = "REGISTRY_RELATION_UNRESOLVED" @dataclass(frozen=True) diff --git a/converters/semantido/src/ossie_semantido/loaders.py b/converters/semantido/src/ossie_semantido/loaders.py index eba7c85a..cb72c26a 100644 --- a/converters/semantido/src/ossie_semantido/loaders.py +++ b/converters/semantido/src/ossie_semantido/loaders.py @@ -42,5 +42,10 @@ def load_from_module(module_path: str, sys_path: str | None = None) -> SemanticL """ if sys_path: sys.path.insert(0, str(Path(sys_path).resolve())) - importlib.import_module(module_path) - return SemanticDeclarativeBase.sync_semantic_layer() + module = importlib.import_module(module_path) + # Convention: a module-level CONCEPT_REGISTRY (semantido + # ConceptRegistry, v0.4.0+) is passed to the sync so concept + # bindings are validated and the registry — definitions, + # relations, external mappings, grain — travels with the layer. + registry = getattr(module, "CONCEPT_REGISTRY", None) + return SemanticDeclarativeBase.sync_semantic_layer(concept_registry=registry) diff --git a/converters/semantido/src/ossie_semantido/osi_to_semantido.py b/converters/semantido/src/ossie_semantido/osi_to_semantido.py deleted file mode 100644 index 8aa9eb0c..00000000 --- a/converters/semantido/src/ossie_semantido/osi_to_semantido.py +++ /dev/null @@ -1,164 +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. - -"""Apache Ossie document -> generated semantido model code. - -This direction is code generation: it emits a Python module of -``@semantic_table``-decorated SQLAlchemy models. Constructs with no -semantido equivalent (metrics, non-ANSI dialect expressions) are dropped -with a recorded ConverterIssue and listed in a TODO comment block at the -top of the generated file. -""" - -import json -from typing import List - -from ossie import OSIDataset, OSIDocument, OSIField - -from ossie_semantido.constants import VENDOR_NAME -from ossie_semantido.converter_issues import ( - ConverterIssue, - ConverterIssueType, - ConverterResult, -) - -_TYPE_MAP = { - "VARCHAR": "String", - "CHAR": "String", - "TEXT": "String", - "INTEGER": "Integer", - "BIGINT": "Integer", - "SMALLINT": "Integer", - "NUMERIC": "Numeric", - "DECIMAL": "Numeric", - "FLOAT": "Float", - "DATE": "Date", - "DATETIME": "DateTime", - "TIMESTAMP": "DateTime", - "BOOLEAN": "Boolean", -} - - -def _vendor_payload(entity) -> dict: - for ext in entity.custom_extensions or []: - if ext.vendor_name == VENDOR_NAME: - try: - return json.loads(ext.data) - except (TypeError, ValueError): - return {} - return {} - - -def _sqlalchemy_type(field: OSIField, issues: List[ConverterIssue]) -> str: - data_type = _vendor_payload(field).get("data_type", "") - base = data_type.split("(")[0].strip().upper() - if base in _TYPE_MAP: - return _TYPE_MAP[base] - if base: - issues.append( - ConverterIssue( - ConverterIssueType.UNMAPPED_DATA_TYPE, f"{field.name}:{base}" - ) - ) - return "String" - - -def _ai(entity): - ctx = entity.ai_context - if ctx is None or isinstance(ctx, str): - return None - return ctx - - -def _class_name(dataset_name: str) -> str: - return "".join(part.capitalize() for part in dataset_name.split("_")) - - -def _emit_dataset(dataset: OSIDataset, issues: List[ConverterIssue]) -> str: - payload = _vendor_payload(dataset) - ctx = _ai(dataset) - - decorator_kwargs = [] - if dataset.description: - decorator_kwargs.append(f" description={dataset.description!r},") - if ctx and ctx.instructions: - decorator_kwargs.append(f" business_context={ctx.instructions!r},") - if ctx and ctx.synonyms: - decorator_kwargs.append(f" synonyms={list(ctx.synonyms)!r},") - if payload.get("sql_filters"): - decorator_kwargs.append(f" sql_filters={payload['sql_filters']!r},") - if payload.get("time_dimension"): - decorator_kwargs.append(f" time_dimension={payload['time_dimension']!r},") - - lines = ["@semantic_table("] + decorator_kwargs + [")"] - lines.append(f"class {_class_name(dataset.name)}(SemanticDeclarativeBase):") - lines.append(f' __tablename__ = "{dataset.name}"') - lines.append("") - - primary_keys = set(dataset.primary_key or []) - for field in dataset.fields or []: - field_payload = _vendor_payload(field) - field_ctx = _ai(field) - sa_type = _sqlalchemy_type(field, issues) - pk = ", primary_key=True" if field.name in primary_keys else "" - lines.append(f" {field.name} = Column({sa_type}{pk})") - if field.description: - lines.append(f" {field.name}_description = {field.description!r}") - if field_payload.get("privacy_level"): - level = field_payload["privacy_level"].upper() - lines.append(f" {field.name}_privacy_level = PrivacyLevel.{level}") - if field_ctx and field_ctx.examples: - lines.append( - f" {field.name}_sample_values = {[str(e) for e in field_ctx.examples]!r}" - ) - if field_payload.get("time_grain"): - grain = field_payload["time_grain"].upper() - lines.append(f" {field.name}_time_grain = TimeGrain.{grain}") - return "\n".join(lines) - - -def osi_to_semantido_source(document: OSIDocument) -> ConverterResult[str]: - """Generate semantido model source code from an Ossie document.""" - issues: List[ConverterIssue] = [] - blocks = [] - - for model in document.semantic_model: - for metric in model.metrics or []: - issues.append( - ConverterIssue(ConverterIssueType.METRIC_DROPPED, metric.name) - ) - for dataset in model.datasets: - blocks.append(_emit_dataset(dataset, issues)) - - todo_block = "" - if issues: - todo_lines = "\n".join( - f"# {i.issue_type.value}: {i.element_name}" for i in issues - ) - todo_block = ( - "# TODO(ossie-semantido): the following Ossie constructs have no\n" - "# semantido equivalent yet and were not converted:\n" + todo_lines + "\n\n" - ) - - header = ( - '"""semantido models generated by ossie-semantido. Review before use."""\n\n' - "from sqlalchemy import Boolean, Column, Date, DateTime, Float, Integer, Numeric, String\n\n" - "from semantido import SemanticDeclarativeBase, semantic_table\n" - "from semantido.generators.semantic_layer import PrivacyLevel, TimeGrain\n\n\n" - ) - source = header + todo_block + "\n\n\n".join(blocks) + "\n" - return ConverterResult(output=source, issues=issues) diff --git a/converters/semantido/src/ossie_semantido/ossie_to_semantido.py b/converters/semantido/src/ossie_semantido/ossie_to_semantido.py new file mode 100644 index 00000000..0af466d1 --- /dev/null +++ b/converters/semantido/src/ossie_semantido/ossie_to_semantido.py @@ -0,0 +1,344 @@ +# 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. + +"""Apache Ossie document -> generated semantido model code. + +This direction is code generation: it emits a Python module of +``@semantic_table``-decorated SQLAlchemy models. Constructs with no +semantido equivalent (metrics, non-ANSI dialect expressions) are dropped +with a recorded ConverterIssue and listed in a TODO comment block at the +top of the generated file. +""" + +import json +from typing import List + +from ossie import OSIDataset, OSIDocument, OSIField + +from ossie_semantido.constants import VENDOR_NAME +from ossie_semantido.converter_issues import ( + ConverterIssue, + ConverterIssueType, + ConverterResult, +) + +_TYPE_MAP = { + "VARCHAR": "String", + "CHAR": "String", + "TEXT": "String", + "INTEGER": "Integer", + "BIGINT": "Integer", + "SMALLINT": "Integer", + "NUMERIC": "Numeric", + "DECIMAL": "Numeric", + "FLOAT": "Float", + "DATE": "Date", + "DATETIME": "DateTime", + "TIMESTAMP": "DateTime", + "BOOLEAN": "Boolean", +} + + +def _vendor_payload(entity) -> dict: + for ext in entity.custom_extensions or []: + if ext.vendor_name == VENDOR_NAME: + try: + return json.loads(ext.data) + except (TypeError, ValueError): + return {} + return {} + + +def _sqlalchemy_type(field: OSIField, issues: List[ConverterIssue]) -> str: + data_type = _vendor_payload(field).get("data_type", "") + base = data_type.split("(")[0].strip().upper() + if base in _TYPE_MAP: + return _TYPE_MAP[base] + if base: + issues.append( + ConverterIssue( + ConverterIssueType.UNMAPPED_DATA_TYPE, f"{field.name}:{base}" + ) + ) + return "String" + + +def _ai(entity): + ctx = entity.ai_context + if ctx is None or isinstance(ctx, str): + return None + return ctx + + +def _class_name(dataset_name: str) -> str: + return "".join(part.capitalize() for part in dataset_name.split("_")) + + +def _emit_dataset(dataset: OSIDataset, issues: List[ConverterIssue]) -> str: + payload = _vendor_payload(dataset) + ctx = _ai(dataset) + + decorator_kwargs = [] + if dataset.description: + decorator_kwargs.append(f" description={dataset.description!r},") + # Prefer the vendor payload's authored split (v0.5.2 converter); + # fall back to ai_context.instructions for documents produced by + # other tools or older converter versions. + if payload.get("business_context"): + decorator_kwargs.append( + f" business_context={payload['business_context']!r}," + ) + elif ctx and ctx.instructions: + decorator_kwargs.append(f" business_context={ctx.instructions!r},") + if payload.get("application_context"): + decorator_kwargs.append( + f" application_context={payload['application_context']!r}," + ) + if ctx and ctx.synonyms: + decorator_kwargs.append(f" synonyms={list(ctx.synonyms)!r},") + if payload.get("concept"): # semantido v0.4.0+ + decorator_kwargs.append(f" concept={payload['concept']!r},") + if payload.get("sql_filters"): + decorator_kwargs.append(f" sql_filters={payload['sql_filters']!r},") + if payload.get("time_dimension"): + decorator_kwargs.append(f" time_dimension={payload['time_dimension']!r},") + + lines = ["@semantic_table("] + decorator_kwargs + [")"] + lines.append(f"class {_class_name(dataset.name)}(SemanticDeclarativeBase):") + lines.append(f' __tablename__ = "{dataset.name}"') + lines.append("") + + primary_keys = set(dataset.primary_key or []) + for field in dataset.fields or []: + field_payload = _vendor_payload(field) + field_ctx = _ai(field) + sa_type = _sqlalchemy_type(field, issues) + pk = ", primary_key=True" if field.name in primary_keys else "" + fk = "" + if field_payload.get("references"): + fk = f', ForeignKey("{field_payload["references"]}")' + lines.append(f" {field.name} = Column({sa_type}{fk}{pk})") + if field.description: + lines.append(f" {field.name}_description = {field.description!r}") + if field_ctx and field_ctx.synonyms: + lines.append(f" {field.name}_synonyms = {list(field_ctx.synonyms)!r}") + if field_payload.get("concept"): # semantido v0.4.0+ + lines.append(f" {field.name}_concept = {field_payload['concept']!r}") + if field_payload.get("application_rules"): + lines.append( + f" {field.name}_application_rules = " + f"{list(field_payload['application_rules'])!r}" + ) + if field_payload.get("privacy_level"): + level = field_payload["privacy_level"].upper() + lines.append(f" {field.name}_privacy_level = PrivacyLevel.{level}") + if field_ctx and field_ctx.examples: + lines.append( + f" {field.name}_sample_values = {[str(e) for e in field_ctx.examples]!r}" + ) + if field_payload.get("time_grain"): + grain = field_payload["time_grain"].upper() + lines.append(f" {field.name}_time_grain = TimeGrain.{grain}") + if payload.get("unique_keys"): # semantido v0.5.0+ + constraints = ", ".join( + "UniqueConstraint(" + ", ".join(repr(c) for c in key) + ")" + for key in payload["unique_keys"] + ) + lines.append("") + lines.append(f" __table_args__ = ({constraints},)") + return "\n".join(lines) + + +_SYMMETRIC_RELATIONS = {"same_as", "related", "distinct_from"} +_ASYMMETRIC_RELATIONS = {"broader", "narrower"} + + +def _emit_registry(registry: dict, issues: List[ConverterIssue]) -> str: + """Generate ``build_registry()`` authoring code from a serialized registry. + + Emission rules follow semantido's authoring semantics: + + - Symmetric relations (``same_as`` / ``related`` / ``distinct_from``) + auto-reciprocate and appear on both concepts in ``to_dict()``, so + each edge is emitted exactly once — on whichever concept is + declared later, when its partner handle already exists. + - Asymmetric relations (``broader`` / ``narrower``) are recorded on + one side only; declaration order is chosen so targets precede + sources. Unresolvable orderings (cycles) drop the relation with a + recorded issue rather than generating code that cannot run. + - ``definition_checksum`` is computed by semantido and never emitted. + """ + concepts = registry.get("concepts", {}) + sources = registry.get("sources", {}) + namespace = registry.get("namespace") + + # Order: satisfy asymmetric-relation dependencies, alphabetical tiebreak. + remaining = dict(sorted(concepts.items())) + ordered: List[str] = [] + emitted = set() + while remaining: + progressed = False + for cid, spec in list(remaining.items()): + deps = { + r["concept"] + for r in spec.get("relations", []) + if r.get("relation") in _ASYMMETRIC_RELATIONS + and r.get("concept") in concepts + } + if deps <= emitted: + ordered.append(cid) + emitted.add(cid) + del remaining[cid] + progressed = True + if not progressed: # cycle: emit rest, relations to later ids drop + ordered.extend(remaining) + emitted.update(remaining) + remaining.clear() + + handles = {cid: f"c_{i}" for i, cid in enumerate(ordered)} + lines = ["def build_registry() -> ConceptRegistry:"] + ns = f"namespace={namespace!r}" if namespace else "" + lines.append(f" registry = ConceptRegistry({ns})") + for name, src in sorted(sources.items()): + kwargs = [ + f"name={name!r}", + f"namespace={src.get('namespace')!r}", + f"version={src.get('version')!r}", + ] + for opt in ("location", "profile"): + if src.get(opt): + kwargs.append(f"{opt}={src[opt]!r}") + lines.append( + " registry.add_source(OntologySource(" + ", ".join(kwargs) + "))" + ) + + declared = set() + for cid in ordered: + spec = concepts[cid] + args = [repr(cid), repr(spec.get("definition", ""))] + if spec.get("label") and spec["label"] != cid: + args.append(f"label={spec['label']!r}") + if spec.get("synonyms"): + args.append(f"synonyms={list(spec['synonyms'])!r}") + if spec.get("grain"): # semantido v0.5.0+ + args.append(f"grain={spec['grain']!r}") + + rel_kwargs: dict = {} + for rel in spec.get("relations", []): + kind, target = rel.get("relation"), rel.get("concept") + if target not in concepts: + issues.append( + ConverterIssue( + ConverterIssueType.REGISTRY_RELATION_UNRESOLVED, + f"{cid}:{kind}:{target}", + ) + ) + continue + if kind in _SYMMETRIC_RELATIONS and target not in declared: + continue # the reciprocal entry on the later concept emits it + if kind in _ASYMMETRIC_RELATIONS and target not in declared: + issues.append( + ConverterIssue( + ConverterIssueType.REGISTRY_RELATION_UNRESOLVED, + f"{cid}:{kind}:{target}", + ) + ) + continue + rel_kwargs.setdefault(kind, []).append(handles[target]) + for kind, targets in rel_kwargs.items(): + value = targets[0] if len(targets) == 1 else "[" + ", ".join(targets) + "]" + args.append(f"{kind}={value}") + + mappings = [] + for m in spec.get("mappings", []): + helper = m.get("relation", "exact_match") + mappings.append(f"{helper}({m.get('source')!r}, {m.get('target')!r})") + if len(mappings) == 1: + args.append(f"external={mappings[0]}") + elif mappings: + args.append("external=[" + ", ".join(mappings) + "]") + + lines.append(f" {handles[cid]} = registry.concept(" + ", ".join(args) + ")") + declared.add(cid) + lines.append(" return registry") + lines.append("") + lines.append("") + lines.append("CONCEPT_REGISTRY = build_registry()") + return "\n".join(lines) + + +def ossie_to_semantido_source(document: OSIDocument) -> ConverterResult[str]: + """Generate semantido model source code from an Ossie document.""" + issues: List[ConverterIssue] = [] + blocks = [] + + registry_block = None + for model in document.semantic_model: + for metric in model.metrics or []: + issues.append( + ConverterIssue(ConverterIssueType.METRIC_DROPPED, metric.name) + ) + model_payload = _vendor_payload(model) + if registry_block is None and model_payload.get("concept_registry"): + registry_block = _emit_registry(model_payload["concept_registry"], issues) + for dataset in model.datasets: + blocks.append(_emit_dataset(dataset, issues)) + if registry_block is not None: + blocks.insert(0, registry_block) + + todo_block = "" + if issues: + todo_lines = "\n".join( + f"# {i.issue_type.value}: {i.element_name}" for i in issues + ) + todo_block = ( + "# TODO(ossie-semantido): the following Ossie constructs have no\n" + "# semantido equivalent yet and were not converted:\n" + todo_lines + "\n\n" + ) + + body = "\n\n\n".join(blocks) + "\n" + sa_imports = [ + "Boolean", + "Column", + "Date", + "DateTime", + "Float", + "Integer", + "Numeric", + "String", + ] + if "ForeignKey(" in body: + sa_imports.append("ForeignKey") + if "UniqueConstraint(" in body: + sa_imports.append("UniqueConstraint") + header_lines = [ + '"""semantido models generated by ossie-semantido. Review before use."""', + "", + "from sqlalchemy import " + ", ".join(sorted(sa_imports)), + "", + "from semantido import SemanticDeclarativeBase, semantic_table", + "from semantido.generators.semantic_layer import PrivacyLevel, TimeGrain", + ] + if registry_block is not None: + header_lines.append( + "from semantido.concepts import (" + "ConceptRegistry, OntologySource, " + "exact_match, close_match, narrow_match, broad_match, related_match)" + ) + header = "\n".join(header_lines) + "\n\n\n" + source = header + todo_block + body + return ConverterResult(output=source, issues=issues) diff --git a/converters/semantido/src/ossie_semantido/semantido_to_osi.py b/converters/semantido/src/ossie_semantido/semantido_to_ossie.py similarity index 81% rename from converters/semantido/src/ossie_semantido/semantido_to_osi.py rename to converters/semantido/src/ossie_semantido/semantido_to_ossie.py index 9cf55689..b5549884 100644 --- a/converters/semantido/src/ossie_semantido/semantido_to_osi.py +++ b/converters/semantido/src/ossie_semantido/semantido_to_ossie.py @@ -93,6 +93,13 @@ def _column_to_field(column: Column) -> OSIField: extension_payload["data_type"] = column.data_type if column.references: extension_payload["references"] = column.references + if column.concept: # semantido v0.4.0+ + extension_payload["concept"] = column.concept + if column.application_rules: + # Also joined into ai_context.instructions above for Ossie-native + # consumers; kept verbatim here so the reverse direction can + # regenerate the authored list. + extension_payload["application_rules"] = list(column.application_rules) return OSIField( name=column.name, @@ -122,6 +129,17 @@ def _table_to_dataset(table: Table) -> OSIDataset: extension_payload["sql_filters"] = table.sql_filters if table.time_dimension: extension_payload["time_dimension"] = table.time_dimension + if table.concept: # semantido v0.4.0+ + extension_payload["concept"] = table.concept + if table.unique_keys: # semantido v0.5.0+ + extension_payload["unique_keys"] = [list(k) for k in table.unique_keys] + # ai_context.instructions joins business + application context for + # Ossie-native consumers; the split is preserved here, so the reverse + # direction regenerates the authored attributes rather than merging. + if table.business_context: + extension_payload["business_context"] = table.business_context + if table.application_context: + extension_payload["application_context"] = table.application_context return OSIDataset( name=table.name, @@ -136,7 +154,7 @@ def _table_to_dataset(table: Table) -> OSIDataset: ) -def _relationship_to_osi( +def _relationship_to_ossie( rel: Relationship, issues: List[ConverterIssue] ) -> Optional[OSIRelationship]: match = _JOIN_RE.match(rel.join_condition or "") @@ -188,7 +206,7 @@ def _relationship_to_osi( ) -def semantic_layer_to_osi( +def semantic_layer_to_ossie( layer: SemanticLayer, model_name: str, description: Optional[str] = None ) -> ConverterResult[OSIDocument]: """Convert a synced semantido SemanticLayer into an Ossie document.""" @@ -198,15 +216,19 @@ def semantic_layer_to_osi( relationships = [] for rel in layer.relationships: - converted = _relationship_to_osi(rel, issues) + converted = _relationship_to_ossie(rel, issues) if converted is not None: relationships.append(converted) - model_extensions = None + model_payload = {} if layer.application_glossary: - model_extensions = [ - _extension({"application_glossary": layer.application_glossary}) - ] + model_payload["application_glossary"] = layer.application_glossary + if layer.concept_registry is not None: # semantido v0.4.0+ + # The registry is the meaning artifact: definitions, relations, + # external mappings, and grain (v0.5.0). Serialized whole so a + # round-trip regenerates the authoring code. + model_payload["concept_registry"] = layer.concept_registry.to_dict() + model_extensions = [_extension(model_payload)] if model_payload else None model = OSISemanticModel( name=model_name, diff --git a/converters/semantido/tests/fixtures/emir_reporting/models/emir_reporting.py b/converters/semantido/tests/fixtures/emir_reporting/models/emir_reporting.py index 2485e3af..7062b251 100644 --- a/converters/semantido/tests/fixtures/emir_reporting/models/emir_reporting.py +++ b/converters/semantido/tests/fixtures/emir_reporting/models/emir_reporting.py @@ -71,9 +71,7 @@ class Trade(SemanticDeclarativeBase): uti_description = "Unique Trade Identifier (Art. 9 REFIT). Canonical trade key." asset_class = Column(String(4), nullable=False) - asset_class_description = ( - "EMIR asset class: 'IR' rates, 'CR' credit, 'EQ' equity, 'FX' FX, 'CO' commodity." - ) + asset_class_description = "EMIR asset class: 'IR' rates, 'CR' credit, 'EQ' equity, 'FX' FX, 'CO' commodity." asset_class_sample_values = ["IR", "FX"] notional_amount = Column(Numeric(20, 2), nullable=False) @@ -88,7 +86,9 @@ class Trade(SemanticDeclarativeBase): notional_currency_sample_values = ["EUR", "USD"] execution_timestamp = Column(DateTime, nullable=False) - execution_timestamp_description = "UTC execution time (Art. 9 REFIT). Primary time axis." + execution_timestamp_description = ( + "UTC execution time (Art. 9 REFIT). Primary time axis." + ) execution_timestamp_time_grain = TimeGrain.SECOND states = relationship("TradeState", back_populates="trade") @@ -190,7 +190,9 @@ class CollateralReport(SemanticDeclarativeBase): reporting_date_time_grain = TimeGrain.DAY initial_margin_posted = Column(Numeric(20, 2)) - initial_margin_posted_description = "IM posted BY the reporting counterparty. Unsigned." + initial_margin_posted_description = ( + "IM posted BY the reporting counterparty. Unsigned." + ) initial_margin_received = Column(Numeric(20, 2)) initial_margin_received_description = ( "IM received. Unsigned; do not sum with posted — subtract." diff --git a/converters/semantido/tests/test_osi_to_semantido.py b/converters/semantido/tests/test_ossie_to_semantido.py similarity index 68% rename from converters/semantido/tests/test_osi_to_semantido.py rename to converters/semantido/tests/test_ossie_to_semantido.py index b03f8ad1..4610e2fe 100644 --- a/converters/semantido/tests/test_osi_to_semantido.py +++ b/converters/semantido/tests/test_ossie_to_semantido.py @@ -15,23 +15,26 @@ # specific language governing permissions and limitations # under the License. -from ossie_semantido.osi_to_semantido import osi_to_semantido_source -from ossie_semantido.semantido_to_osi import semantic_layer_to_osi +from ossie_semantido.ossie_to_semantido import ossie_to_semantido_source +from ossie_semantido.semantido_to_ossie import semantic_layer_to_ossie from tests.helpers import load_emir_layer def test_generated_source_compiles(): layer = load_emir_layer() - document = semantic_layer_to_osi(layer, model_name="emir_reporting").output - result = osi_to_semantido_source(document) + document = semantic_layer_to_ossie(layer, model_name="emir_reporting").output + result = ossie_to_semantido_source(document) compile(result.output, "", "exec") def test_roundtrip_preserves_governance_annotations(): layer = load_emir_layer() - document = semantic_layer_to_osi(layer, model_name="emir_reporting").output - source = osi_to_semantido_source(document).output + document = semantic_layer_to_ossie(layer, model_name="emir_reporting").output + source = ossie_to_semantido_source(document).output assert "sql_filters=" in source assert "PrivacyLevel.CONFIDENTIAL" in source - assert 'time_dimension="reporting_date"' in source or "time_dimension='reporting_date'" in source + assert ( + 'time_dimension="reporting_date"' in source + or "time_dimension='reporting_date'" in source + ) assert "TimeGrain.DAY" in source diff --git a/converters/semantido/tests/test_semantido_to_osi.py b/converters/semantido/tests/test_semantido_to_ossie.py similarity index 82% rename from converters/semantido/tests/test_semantido_to_osi.py rename to converters/semantido/tests/test_semantido_to_ossie.py index 4b65a666..4111ce63 100644 --- a/converters/semantido/tests/test_semantido_to_osi.py +++ b/converters/semantido/tests/test_semantido_to_ossie.py @@ -19,7 +19,7 @@ from ossie import OSIDocument -from ossie_semantido.semantido_to_osi import VENDOR_NAME, semantic_layer_to_osi +from ossie_semantido.semantido_to_ossie import VENDOR_NAME, semantic_layer_to_ossie from tests.helpers import load_emir_layer @@ -32,14 +32,14 @@ def _vendor_data(entity): def test_converts_all_tables_to_datasets(): layer = load_emir_layer() - result = semantic_layer_to_osi(layer, model_name="emir_reporting") + result = semantic_layer_to_ossie(layer, model_name="emir_reporting") model = result.output.semantic_model[0] assert {d.name for d in model.datasets} == set(layer.tables) def test_relationships_are_parsed_not_dropped(): layer = load_emir_layer() - result = semantic_layer_to_osi(layer, model_name="emir_reporting") + result = semantic_layer_to_ossie(layer, model_name="emir_reporting") model = result.output.semantic_model[0] assert model.relationships, "expected FK relationships in the fixture" assert not result.issues, f"unexpected conversion issues: {result.issues}" @@ -49,7 +49,9 @@ def test_relationships_are_parsed_not_dropped(): def test_governance_metadata_preserved_in_extensions(): layer = load_emir_layer() - model = semantic_layer_to_osi(layer, model_name="emir_reporting").output.semantic_model[0] + model = semantic_layer_to_ossie( + layer, model_name="emir_reporting" + ).output.semantic_model[0] trade_state = next(d for d in model.datasets if d.name == "trade_state") assert _vendor_data(trade_state).get("sql_filters"), "sql_filters must survive" @@ -62,7 +64,9 @@ def test_governance_metadata_preserved_in_extensions(): def test_time_dimension_marked_on_field(): layer = load_emir_layer() - model = semantic_layer_to_osi(layer, model_name="emir_reporting").output.semantic_model[0] + model = semantic_layer_to_ossie( + layer, model_name="emir_reporting" + ).output.semantic_model[0] trade = next(d for d in model.datasets if d.name == "trade") ts_field = next(f for f in trade.fields if f.name == "execution_timestamp") assert ts_field.dimension is not None and ts_field.dimension.is_time is True @@ -70,7 +74,9 @@ def test_time_dimension_marked_on_field(): def test_yaml_output_is_valid_document(): layer = load_emir_layer() - yaml_text = semantic_layer_to_osi(layer, model_name="emir_reporting").output.to_osi_yaml() + yaml_text = semantic_layer_to_ossie( + layer, model_name="emir_reporting" + ).output.to_osi_yaml() import yaml as pyyaml OSIDocument.model_validate(pyyaml.safe_load(yaml_text)) diff --git a/converters/semantido/tests/test_v05_roundtrip.py b/converters/semantido/tests/test_v05_roundtrip.py new file mode 100644 index 00000000..9b8a2305 --- /dev/null +++ b/converters/semantido/tests/test_v05_roundtrip.py @@ -0,0 +1,140 @@ +# 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 v0.4/v0.5 surface — concept bindings, the concept registry +(with grain), and unique keys — carried into the Ossie document and +regenerated by the reverse direction.""" + +import json + +from sqlalchemy import Column, ForeignKey, Integer, String, UniqueConstraint +from sqlalchemy.orm import DeclarativeBase + +from semantido import ConceptRegistry, SemanticBase, semantic_table +from ossie_semantido.constants import VENDOR_NAME +from ossie_semantido.ossie_to_semantido import ossie_to_semantido_source +from ossie_semantido.semantido_to_ossie import semantic_layer_to_ossie + + +class _Base(SemanticBase, DeclarativeBase): + """Isolated base so this module does not pollute the shared registry.""" + + +def _registry() -> ConceptRegistry: + registry = ConceptRegistry(namespace="secmaster") + isin = registry.concept("isin", "Issue-level instrument identifier.", grain="issue") + registry.concept( + "figi", + "Listing-level instrument identifier.", + grain="listing", + distinct_from=isin, + ) + return registry + + +@semantic_table( + description="One row per security issue.", + application_context="Golden record; downstream systems read only.", + concept="isin", +) +class Security(_Base): + __tablename__ = "security_master" + + id = Column(Integer, primary_key=True) + primary_isin = Column(String(12)) + primary_isin_concept = "isin" + figi = Column(String(12)) + figi_concept = "figi" + + __table_args__ = (UniqueConstraint("primary_isin"),) + + +@semantic_table(description="Vendor feed rows.") +class Feed(_Base): + __tablename__ = "bbg_feed" + + id = Column(Integer, primary_key=True) + security_id = Column(Integer, ForeignKey("security_master.id")) + + +def _payload(entity) -> dict: + for ext in entity.custom_extensions or []: + if ext.vendor_name == VENDOR_NAME: + return json.loads(ext.data) + return {} + + +def _document(): + layer = _Base.sync_semantic_layer(concept_registry=_registry()) + return semantic_layer_to_ossie(layer, model_name="secmaster").output + + +def test_forward_carries_v05_fields(): + document = _document() + model = document.semantic_model[0] + + model_payload = _payload(model) + registry = model_payload["concept_registry"] + assert registry["namespace"] == "secmaster" + assert registry["concepts"]["isin"]["grain"] == "issue" + assert { + (r["relation"], r["concept"]) for r in registry["concepts"]["figi"]["relations"] + } == {("distinct_from", "isin")} + + datasets = {d.name: d for d in model.datasets} + table_payload = _payload(datasets["security_master"]) + assert table_payload["concept"] == "isin" + assert table_payload["unique_keys"] == [["primary_isin"]] + assert table_payload["application_context"].startswith("Golden record") + + fields = {f.name: f for f in datasets["security_master"].fields} + assert _payload(fields["primary_isin"])["concept"] == "isin" + assert _payload(fields["figi"])["concept"] == "figi" + + +def test_reverse_regenerates_v05_authoring_surface(): + result = ossie_to_semantido_source(_document()) + source = result.output + + assert "concept='isin'," in source + assert "primary_isin_concept = 'isin'" in source + assert "figi_concept = 'figi'" in source + assert "UniqueConstraint('primary_isin')" in source + assert "application_context=" in source + assert 'ForeignKey("security_master.id")' in source + assert "grain='issue'" in source + assert "distinct_from=" in source + assert "CONCEPT_REGISTRY = build_registry()" in source + assert "definition_checksum" not in source + assert not result.issues + + +def test_generated_module_executes_and_resyncs(): + source = ossie_to_semantido_source(_document()).output + # The generated module declares against the shared + # SemanticDeclarativeBase; execute in isolation and re-sync. + namespace: dict = {} + exec(compile(source, "", "exec"), namespace) # noqa: S102 + + base = namespace["SemanticDeclarativeBase"] + layer = base.sync_semantic_layer(concept_registry=namespace["CONCEPT_REGISTRY"]) + + table = layer.tables["security_master"] + assert table.concept == "isin" + assert table.unique_keys == [["primary_isin"]] + assert {c.name: c.concept for c in table.columns}["primary_isin"] == "isin" + assert layer.concept_registry.concepts["figi"].grain == "listing" diff --git a/converters/semantido/uv.lock b/converters/semantido/uv.lock new file mode 100644 index 00000000..43b914d2 --- /dev/null +++ b/converters/semantido/uv.lock @@ -0,0 +1,458 @@ +version = 1 +revision = 3 +requires-python = ">=3.11" + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + +[[package]] +name = "apache-ossie" +version = "0.2.0.dev0" +source = { editable = "../../python" } +dependencies = [ + { name = "pydantic" }, + { name = "pyyaml" }, +] + +[package.metadata] +requires-dist = [ + { name = "pydantic", specifier = ">=2.0" }, + { name = "pyyaml", specifier = ">=6.0" }, +] + +[[package]] +name = "apache-ossie-semantido" +version = "0.1.0.dev0" +source = { editable = "." } +dependencies = [ + { name = "apache-ossie" }, + { name = "pyyaml" }, + { name = "semantido" }, + { name = "sqlalchemy" }, +] + +[package.optional-dependencies] +dev = [ + { name = "pytest" }, + { name = "ruff" }, +] + +[package.metadata] +requires-dist = [ + { name = "apache-ossie", editable = "../../python" }, + { name = "pytest", marker = "extra == 'dev'", specifier = ">=8.0" }, + { name = "pyyaml", specifier = ">=6.0" }, + { name = "ruff", marker = "extra == 'dev'", specifier = "==0.15.22" }, + { name = "semantido", specifier = ">=0.5.2" }, + { name = "sqlalchemy", specifier = ">=2.0" }, +] +provides-extras = ["dev"] + +[[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 = "greenlet" +version = "3.5.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e2/f1/fbbfef6af0bad0548f09bc28948ea3c275b4edb19e17fc5ca9900a6a634d/greenlet-3.5.3.tar.gz", hash = "sha256:a61efc018fd3eb317eeca31aba90ee9e7f26f22884a79b6c6ec715bf71bb62f1", size = 200270, upload-time = "2026-06-26T19:28:24.832Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/58/5404031044f55afad7aad1aff8be3f22b1bed03e237cfeabbc7e5c8cfde0/greenlet-3.5.3-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:aca9b4ce85b152b5524ef7d88170efdff80dc0032aa8b75f9aaf7f3479ea95b4", size = 287424, upload-time = "2026-06-26T18:20:31.469Z" }, + { url = "https://files.pythonhosted.org/packages/b4/bf/1c65e9b94a54d547068fa5b5a8a06f221f3316b48908e08668d29c77cb50/greenlet-3.5.3-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f71be4920368fe1fabeeaa53d1e3548337e2b223d9565f8ad5e392a75ba23fc", size = 606523, upload-time = "2026-06-26T19:07:08.859Z" }, + { url = "https://files.pythonhosted.org/packages/b8/c7/b66baacc95775ad511287acb0137b95574a9ce5491902372b7564799d790/greenlet-3.5.3-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4d77e67f65f98449e3fb83f795b5d0a8437aead2f874ca89c96576caf4be3af6", size = 618315, upload-time = "2026-06-26T19:10:06.055Z" }, + { url = "https://files.pythonhosted.org/packages/78/2b/28ed29463522fdbe4c15b1f63922041626a7478316b34ab4adda3f0a4aba/greenlet-3.5.3-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8540f1e6205bd13ca0ce685581037219ca54a1b41a0a15d228c6c9b8ad5903d7", size = 617381, upload-time = "2026-06-26T18:32:16.077Z" }, + { url = "https://files.pythonhosted.org/packages/2a/7b/ad04e9d1337fc04965dc9fc616b6a72cb65a24b800a014c011ec812f5489/greenlet-3.5.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7ef56fe650f50575bf843acde967b9c567687f3c22340941a899b7bc56e956a8", size = 1577771, upload-time = "2026-06-26T19:09:01.537Z" }, + { url = "https://files.pythonhosted.org/packages/d8/33/6c87ab7ba663f70ca21f3022aad1ffe56d3f3e0521e836c2415e13abcc3c/greenlet-3.5.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5121af01cf911e70056c00d4b46d5e9b5d1415550038573d744138bacb59e6b8", size = 1644048, upload-time = "2026-06-26T18:31:42.996Z" }, + { url = "https://files.pythonhosted.org/packages/1c/35/f0d8ee998b422cf8693b270f098e55d8d4ec8006b061b333f54f177d28d9/greenlet-3.5.3-cp311-cp311-win_amd64.whl", hash = "sha256:0f41e4a05a3c0cb31b17023eff28dd111e1d16bf7d7d00406cd7df23f31398a7", size = 239137, upload-time = "2026-06-26T18:23:21.664Z" }, + { url = "https://files.pythonhosted.org/packages/fb/96/b9820295576ef18c9edc404f10e260ae7215ceaf3781a54b720ed2627862/greenlet-3.5.3-cp311-cp311-win_arm64.whl", hash = "sha256:ec6f1af59f6b5f3fc9678e2ea062d8377d22ac644f7844cb7a292910cf12ff44", size = 237630, upload-time = "2026-06-26T18:24:00.281Z" }, + { url = "https://files.pythonhosted.org/packages/5d/6e/4c37d51a2b7f82d2ff11bb6b5f7d766d9a011726624af255e843727627a3/greenlet-3.5.3-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:719757059f5a53fd0dde23f78cffeafcdd97b21c850ddb7ca684a3c1a1f122e2", size = 288685, upload-time = "2026-06-26T18:22:08.977Z" }, + { url = "https://files.pythonhosted.org/packages/7a/73/815dd90131c1b71ebdf53dbc7c276cafec2a1173b97559f97aba72724a87/greenlet-3.5.3-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:efa9f765dd09f9d0cdac651ffdf631ee59ec5dc6ee7a73e0c012ba9c52fbdf5b", size = 604761, upload-time = "2026-06-26T19:07:10.114Z" }, + { url = "https://files.pythonhosted.org/packages/9f/57/079cfe76bcef36b153b25607ee91c6fcb58f17f8b23c86bbbeabe0c88d72/greenlet-3.5.3-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7faba15ac005376e02a0384504e0243be3370ce010296a44a820feb342b505ab", size = 617044, upload-time = "2026-06-26T19:10:07.25Z" }, + { url = "https://files.pythonhosted.org/packages/37/87/b4d095775a3fb1bcafbb483fc206b27ebb785724c83051447737085dc54e/greenlet-3.5.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:87142215824be6ac05e2e8e2786eec307ccbc27c36723c3881959df654af6861", size = 614244, upload-time = "2026-06-26T18:32:17.594Z" }, + { url = "https://files.pythonhosted.org/packages/8a/70/7559b609683650fa2b95b8ab84b4ab0b26556a635d19675e12aa832d826d/greenlet-3.5.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:215275b1b49320987352e6c1b054acca0064f965a2c66992bed9a6f7d913f149", size = 1574210, upload-time = "2026-06-26T19:09:03.077Z" }, + { url = "https://files.pythonhosted.org/packages/ae/73/be55392074c60fc37655ca40fa6022457bfbf6718e9e342a7b0b41f96dd2/greenlet-3.5.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6b1b0eed82364b0e32c4ea0f221452d33e6bb17ae094d9f72aed9851812747ea", size = 1638627, upload-time = "2026-06-26T18:31:44.748Z" }, + { url = "https://files.pythonhosted.org/packages/14/40/c57489acf8e37d74e2913d4eff63aa0dba17acccc4bdeef874dde2dbbec9/greenlet-3.5.3-cp312-cp312-win_amd64.whl", hash = "sha256:cde8adafa2365676f74a979744629589999093bc86e2484214f58e61df08902c", size = 239882, upload-time = "2026-06-26T18:23:27.518Z" }, + { url = "https://files.pythonhosted.org/packages/71/fd/6fea0e3d6600f785069481ee637e09378dd4118acdfd38ad88ae2db31c98/greenlet-3.5.3-cp312-cp312-win_arm64.whl", hash = "sha256:c4e7b79d83805475f0102008843f6eb45fd3bb0b2e88c774adab5fbaab27117d", size = 238211, upload-time = "2026-06-26T18:22:37.671Z" }, + { url = "https://files.pythonhosted.org/packages/9b/ff/a620267401db30a50cc8450ee90730e2d4a85658c055c0e760d4ed47fb13/greenlet-3.5.3-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:c8d87c2134d871df96ecdea9cec7cbaab286dadab0f56476e57aaf9e8ac11550", size = 287609, upload-time = "2026-06-26T18:21:14.724Z" }, + { url = "https://files.pythonhosted.org/packages/d6/fa/5401ac78021c826a25b6dde0c705e0a8f29b617509f9185a31dac15fbe1b/greenlet-3.5.3-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a2d185dd1621757e70c3861cceffd5317ab4e7ed7eb09c82994828468527ade5", size = 607435, upload-time = "2026-06-26T19:07:11.412Z" }, + { url = "https://files.pythonhosted.org/packages/e9/76/1dc144a2e56e65d36405078ed774224375ea520a1870a6e46e08bb4ac7bf/greenlet-3.5.3-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1c514a468149bf8fbbab874188a3535cd8a48a3e353eb53a3d424296f8dbacd3", size = 619787, upload-time = "2026-06-26T19:10:08.396Z" }, + { url = "https://files.pythonhosted.org/packages/bf/87/c298cee62df1de4ad7fec32abda73526cff347fd143a6ed4ac369246668a/greenlet-3.5.3-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:915f887cf2682b66419b879423a2e072634aa7b7dce6f3ada4957cfced3f1e9a", size = 616786, upload-time = "2026-06-26T18:32:19.128Z" }, + { url = "https://files.pythonhosted.org/packages/9e/2e/e6f009885ed0705ccf33fe0583c117cfd03cde77e31a596dd5785a30762b/greenlet-3.5.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:766cfd421c13e450feb340cd472a3ed9957d438727b7b4593ad7c76c5d2b0deb", size = 1574316, upload-time = "2026-06-26T19:09:04.273Z" }, + { url = "https://files.pythonhosted.org/packages/ef/fe/43fd110b01e40da0adb7c90ac7ea744bef2d43dca00de5095fd2351c2a68/greenlet-3.5.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2ecda9ec22edf38fa389369eaed8c3d37c05f3c54e69f69438dbb2cc1de1458b", size = 1638614, upload-time = "2026-06-26T18:31:46.297Z" }, + { url = "https://files.pythonhosted.org/packages/0f/7c/062447147a61f8b4337b156fe70d32a165fcf2f89d7ca6255e572806705c/greenlet-3.5.3-cp313-cp313-win_amd64.whl", hash = "sha256:c82304750f057167ff60d188df1d0cc1764ce9567eadf03e6a7443bcedd0b30b", size = 239850, upload-time = "2026-06-26T18:21:54.613Z" }, + { url = "https://files.pythonhosted.org/packages/c7/7e/220a7f5824a64a60443fc03b39dfac4ea63a7fb6d481efa27eafa928e7f4/greenlet-3.5.3-cp313-cp313-win_arm64.whl", hash = "sha256:dc133a1569ee667b2a6ef56ce551084aeefd87a5acbc4736d336d1e2edc6cfc4", size = 238141, upload-time = "2026-06-26T18:22:48.507Z" }, + { url = "https://files.pythonhosted.org/packages/c3/93/43e116ee114b28737ba7e12952a0d4e2f55944d0f84e42bc91ba7192a3c9/greenlet-3.5.3-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:fd2e02fa07485778536a036222d616ab957b1d533f36b3ed98ce725d9c9d3117", size = 288202, upload-time = "2026-06-26T18:23:49.604Z" }, + { url = "https://files.pythonhosted.org/packages/82/2f/146d218299046a43d1f029fd544b3d110d0f175a09c715c7e8da4a4a345d/greenlet-3.5.3-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:df0a0628d1597eb0897b62f55d1343f772405fd25f3b2a796c76874b0c2e22e8", size = 654096, upload-time = "2026-06-26T19:07:12.71Z" }, + { url = "https://files.pythonhosted.org/packages/a0/cc/04738cafb3f45fa991ea44f9de94c47dcec964f5a972300988a6751f49d9/greenlet-3.5.3-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ebd933a6adabc298bab47731a130fe6bfb888bd934eee37810f151159544540d", size = 666304, upload-time = "2026-06-26T19:10:09.503Z" }, + { url = "https://files.pythonhosted.org/packages/ce/aa/4e0dad5e605c270c784ab911c43da6adb136ccd4d81180f763ca429a723d/greenlet-3.5.3-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4b9d501b40e80b70e32323c799dd9b420a5577a9601469d362ae1ffb690f3a7c", size = 663635, upload-time = "2026-06-26T18:32:20.802Z" }, + { url = "https://files.pythonhosted.org/packages/d1/50/13efdbea246fe3d3b735e191fec08fb50809f53cd2383ebe123d0809e44b/greenlet-3.5.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a1fad1d11e7d6aab184107baa8e4ece11ccba3ec9599cd7efa5ff4d70d43256a", size = 1621252, upload-time = "2026-06-26T19:09:05.647Z" }, + { url = "https://files.pythonhosted.org/packages/f7/22/c0a336ae4a1410fd5f5121098e5bfbf1865f64c5ef80b4b5412886c4a332/greenlet-3.5.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:fad5aec764399f1b5cc347ad250a59660f20c8f8888ea6bae1f93b769cce1154", size = 1684824, upload-time = "2026-06-26T18:31:47.738Z" }, + { url = "https://files.pythonhosted.org/packages/7a/94/91aec0030bea75c4b3244251d0de60a1f3432d1ecb53ab6c437fb5c3ba61/greenlet-3.5.3-cp314-cp314-win_amd64.whl", hash = "sha256:7669aa24cf2a1041d6f7899575b494a3ab4cf68bfcc8609b1dc0be7272db835e", size = 240754, upload-time = "2026-06-26T18:22:15.669Z" }, + { url = "https://files.pythonhosted.org/packages/e5/06/68d0983e79e02138f64b4d303c500c27ddb48e5e77f3debb80888a921eae/greenlet-3.5.3-cp314-cp314-win_arm64.whl", hash = "sha256:5b4807c4082c9d1b6d9eed56fcd041863e37f2228106eef24c30ca096e238605", size = 239549, upload-time = "2026-06-26T18:22:42.996Z" }, + { url = "https://files.pythonhosted.org/packages/91/95/3e161213d7f1d378d15aa9e792093e9bfe01844680d04b7fd6e0107c9098/greenlet-3.5.3-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:271a8ea7c1024e8a0d7dd2be66dd66dda8a07193f41a17b9e924f7600f5b62be", size = 296389, upload-time = "2026-06-26T18:22:20.657Z" }, + { url = "https://files.pythonhosted.org/packages/00/92/715c44721abe2b4d1ae9abde4179411868a5bff312479f54e105d372f131/greenlet-3.5.3-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:19131729ae0ddc3c2e1ef85e650169b5e37ee32e400f215f78b94d7b0d567310", size = 653382, upload-time = "2026-06-26T19:07:14.209Z" }, + { url = "https://files.pythonhosted.org/packages/a0/83/37a10372a1090a6624cca8e74c12df1a36c2dc36429ed0255b7fb1aeee23/greenlet-3.5.3-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1540dd8e5fc2a5aec40fbb98ef8e149fa47c89a4b4a1cf2575a14d3d1869d7a8", size = 659401, upload-time = "2026-06-26T19:10:10.876Z" }, + { url = "https://files.pythonhosted.org/packages/db/e2/d1509cad4207da559cc42986ecdd8fc67ad0d1bba2bf03023c467fd5e0f3/greenlet-3.5.3-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e81fa194a1d20967877bdf9c7794db2bc99063e5be36aee710c08f04c5bb087f", size = 656969, upload-time = "2026-06-26T18:32:22.272Z" }, + { url = "https://files.pythonhosted.org/packages/86/7d/eaf70de20aadca3a5884aec58362861c64ce45e7b277f47ed026926a3b89/greenlet-3.5.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:55cf4d777485d43110e47133cbba6d74a8885a87ec1227ef0267f9ee80c5aa21", size = 1617822, upload-time = "2026-06-26T19:09:06.893Z" }, + { url = "https://files.pythonhosted.org/packages/8a/f9/414d38fc400ae4350d4185eaad1827676f7cf5287b9136e0ed1cbbe20a7f/greenlet-3.5.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:12a248ba75f6a9a236375f52296c498c89ff1d8badf32deb9eca7abd5853f7da", size = 1677983, upload-time = "2026-06-26T18:31:49.396Z" }, + { url = "https://files.pythonhosted.org/packages/e4/15/7edb977e08f9bff702fe42d6c902702786ff6b9694058b4e6a2a6ac90e57/greenlet-3.5.3-cp314-cp314t-win_amd64.whl", hash = "sha256:efc6bd60ea02e085862c74a3ef64b147ffc6f1a5ea7d9f26e7a939943f68c1e3", size = 243626, upload-time = "2026-06-26T18:24:41.485Z" }, + { url = "https://files.pythonhosted.org/packages/2c/8a/93928dce91e6b3598b5e779e8d1fd6576a504640c58e78627077f6a7a91a/greenlet-3.5.3-cp315-cp315-macosx_11_0_universal2.whl", hash = "sha256:ea03f2f04367845d6b58eeed276e1e56e51f0b97d8ad5a88a7d20a91dc9056cc", size = 288860, upload-time = "2026-06-26T18:22:48.07Z" }, + { url = "https://files.pythonhosted.org/packages/4f/ca/69db42d447a1378043e2c8f19c09cbbd1263371505053c496b49066d3d16/greenlet-3.5.3-cp315-cp315-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:78dbef602fda6d97d957eb7937f70c9ce9e9527330347f8f6b6f9e554a9e7a47", size = 659747, upload-time = "2026-06-26T19:07:15.565Z" }, + { url = "https://files.pythonhosted.org/packages/a8/0b/af7ac2ef8dd41e3da1a40dda6305c23b9a03e13ba975ec916357b50f8575/greenlet-3.5.3-cp315-cp315-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6f73857adb8fee13fa56c172bd11262f888c0c648f9fea113e777bb2c7904a81", size = 670419, upload-time = "2026-06-26T19:10:12.293Z" }, + { url = "https://files.pythonhosted.org/packages/51/1e/1d51640cacbfc455dbe9f9a9f594c49e4e244f63b9971a2f4764e46cc53d/greenlet-3.5.3-cp315-cp315-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:232fec92e823addaf02d9472cf7381e24a1d046a6ced1103c5caa4c21b9dfc1d", size = 668787, upload-time = "2026-06-26T18:32:24.298Z" }, + { url = "https://files.pythonhosted.org/packages/21/66/4030d5b0b5894500023f003bb054d9bb354dfbd1e186c3a296759172f5f5/greenlet-3.5.3-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:2421c3564da9429d5586d46ca31ebb26516b5498a802cf65c041a8e8a8980d34", size = 1626305, upload-time = "2026-06-26T19:09:08.281Z" }, + { url = "https://files.pythonhosted.org/packages/0e/50/5221371c7550108dfa3c378debc41d032aa9c78e89abb01d8011cfc93289/greenlet-3.5.3-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:e0f0d160f0b2e558e6c75f7930967183255dc9735e5f5b8cae58ee09c9576d8b", size = 1688631, upload-time = "2026-06-26T18:31:51.278Z" }, + { url = "https://files.pythonhosted.org/packages/68/5d/00d469daae3c65d2bf620b10eee82eb022127d483c6bc8c69fae6f3fbf17/greenlet-3.5.3-cp315-cp315-win_amd64.whl", hash = "sha256:dd99329bbc15ca78dcc583dba05d0b1b0bae01ab6c2174989f5aaee3e41ac930", size = 241027, upload-time = "2026-06-26T18:22:38.203Z" }, + { url = "https://files.pythonhosted.org/packages/e7/e8/883785b44c5780ed71e83d3e4437e710470be17a2e181e8b601e2da0dc4a/greenlet-3.5.3-cp315-cp315-win_arm64.whl", hash = "sha256:499fef2acede88c1864a57bb586b4bf533c81e1b82df7ab93451cdb47dfec227", size = 240085, upload-time = "2026-06-26T18:23:54.217Z" }, + { url = "https://files.pythonhosted.org/packages/1c/da/4f4a8450962fad137c1c8981a3f1b8919d06c829993d4d476f9c525d5173/greenlet-3.5.3-cp315-cp315t-macosx_11_0_universal2.whl", hash = "sha256:176bc16a721fa5fc294d70b87b4dfa5fbdd251b3da5d5372735ecef9bd7d6d0c", size = 297221, upload-time = "2026-06-26T18:23:27.176Z" }, + { url = "https://files.pythonhosted.org/packages/57/66/b3bfae3e220a9b63ea539a0eea681800c69ab1aada757eae8789f183e7ce/greenlet-3.5.3-cp315-cp315t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:629b614d2b786e89c50440e246f33eea78f58a962d0bdbbcc809e6d13605903f", size = 657221, upload-time = "2026-06-26T19:07:16.973Z" }, + { url = "https://files.pythonhosted.org/packages/7b/81/b6d4d73a709684fc77e7fa034d7c2fe82cffa9fc920fadcaa659c2626213/greenlet-3.5.3-cp315-cp315t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2b2e857ae16f5f72142edf75f9f176fe7526ba19a2841df1420516f83831c9f2", size = 663226, upload-time = "2026-06-26T19:10:13.723Z" }, + { url = "https://files.pythonhosted.org/packages/f5/07/e210b02b589f16e74ff48b730690e4a34ffe984219fce4f3c1a0e7ec8545/greenlet-3.5.3-cp315-cp315t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e515757e2e36bcbf1fad09a46e1557e8b1ae1797d4b44d09da7deed88ad28608", size = 660802, upload-time = "2026-06-26T18:32:26.081Z" }, + { url = "https://files.pythonhosted.org/packages/eb/2e/5303eb3fa06bca089060f479707182a93e360683bc252acf846c3090d34e/greenlet-3.5.3-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:b363d46ed1ea431825fdb01471bb024fc08399bad1572a616e853c7684415adb", size = 1622157, upload-time = "2026-06-26T19:09:09.527Z" }, + { url = "https://files.pythonhosted.org/packages/54/70/50de47a488f14df260b50ae34fb5d56016e308b098eab02c878b5223c26a/greenlet-3.5.3-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:e44da2f5bbdaabaf7d80b73dbb430c7035771e9f244e3c8b769715c9d8fa0a16", size = 1681159, upload-time = "2026-06-26T18:31:52.986Z" }, + { url = "https://files.pythonhosted.org/packages/a7/13/1055e1dda7882073eda533e2b96c62e55bbd2db7fda6d5ece992febc7071/greenlet-3.5.3-cp315-cp315t-win_amd64.whl", hash = "sha256:8ff8bed3e3baa20a3ea261ce00526f1898ad4801d4886fd2220580ee0ad8fadf", size = 244007, upload-time = "2026-06-26T18:22:04.353Z" }, + { url = "https://files.pythonhosted.org/packages/b4/0d/ca7d15afbdc397e3401134c9e1800d51d12b829661786187a4ad08fe484f/greenlet-3.5.3-cp315-cp315t-win_arm64.whl", hash = "sha256:b7068bd09f761f3f5b4d214c2bed063186b2a86148c740b3873e3f56d79bac31", size = 242586, upload-time = "2026-06-26T18:23:37.93Z" }, +] + +[[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" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/18/a5/b60d21ac674192f8ab0ba4e9fd860690f9b4a6e51ca5df118733b487d8d6/pydantic-2.13.4.tar.gz", hash = "sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6", size = 844775, upload-time = "2026-05-06T13:43:05.343Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl", hash = "sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba", size = 472262, upload-time = "2026-05-06T13:43:02.641Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.46.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9d/56/921726b776ace8d8f5db44c4ef961006580d91dc52b803c489fafd1aa249/pydantic_core-2.46.4.tar.gz", hash = "sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1", size = 471464, upload-time = "2026-05-06T13:37:06.98Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5c/fa/6d7708d2cfc1a832acb6aeb0cd16e801902df8a0f583bb3b4b527fde022e/pydantic_core-2.46.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:0e96592440881c74a213e5ad528e2b24d3d4f940de2766bed9010ab1d9e51594", size = 2111872, upload-time = "2026-05-06T13:40:27.596Z" }, + { url = "https://files.pythonhosted.org/packages/ae/6f/aa064a3e74b5745afbdf250594f38e7ead05e2d651bcb35994b9417a0d4d/pydantic_core-2.46.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e0d65b8c354be7fb5f720c3caa8bc940bc2d20ce749c8e06135f07f8ed95dd7c", size = 1948255, upload-time = "2026-05-06T13:39:12.574Z" }, + { url = "https://files.pythonhosted.org/packages/43/3a/41114a9f7569b84b4d84e7a018c57c56347dac30c0d4a872946ec4e36c46/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bfb192b3f4b9e8a89b6277b6ce787564f62cfd272055f6e685726b111dc7826", size = 1972827, upload-time = "2026-05-06T13:38:19.841Z" }, + { url = "https://files.pythonhosted.org/packages/ef/25/1ab42e8048fe551934d9884e8d64daa7e990ad386f310a15981aeb6a5b08/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9037063db01f09b09e237c282b6792bd4da634b5402c4e7f0c61effed7701a04", size = 2041051, upload-time = "2026-05-06T13:38:10.447Z" }, + { url = "https://files.pythonhosted.org/packages/94/c2/1a934597ddf08da410385b3b7aae91956a5a76c635effef456074fad7e88/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fc010ab034c8c7452522748bf937df58020d256ccae0874463d1f4d01758af8e", size = 2221314, upload-time = "2026-05-06T13:40:13.089Z" }, + { url = "https://files.pythonhosted.org/packages/02/6d/9e8ad178c9c4df27ad3c8f25d1fe2a7ab0d2ba0559fad4aee5d3d1f16771/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c5dac79fa1614d1e06ca695109c6105923bd9c7d1d6c918d4e637b7e6b32fd3", size = 2285146, upload-time = "2026-05-06T13:38:59.224Z" }, + { url = "https://files.pythonhosted.org/packages/80/50/540cd3aeefc041beb111125c4bff779831a2111fc6b15a9138cda277d32c/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f9fa868638bf362d3d138ea55829cefb3d5f4b0d7f142234382a15e2485dbec4", size = 2089685, upload-time = "2026-05-06T13:38:17.762Z" }, + { url = "https://files.pythonhosted.org/packages/6b/a4/b440ad35f05f6a38f89fa0f149accb3f0e02be94ca5e15f3c449a61b4bc9/pydantic_core-2.46.4-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:17299feefe090f2caa5b8e37222bb5f663e4935a8bfa6931d4102e5df1a9f398", size = 2115420, upload-time = "2026-05-06T13:37:58.195Z" }, + { url = "https://files.pythonhosted.org/packages/99/61/de4f55db8dfd57bfdfa9a12ec90fe1b57c4f41062f7ca86f08586b3e0ac0/pydantic_core-2.46.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4c63ebc82684aa89d9a3bcbd13d515b3be44250dc68dd3bd81526c1cb31286c3", size = 2165122, upload-time = "2026-05-06T13:37:01.167Z" }, + { url = "https://files.pythonhosted.org/packages/f7/52/7c529d7bdb2d1068bd52f51fe32572c8301f9a4febf1948f10639f1436f5/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:aaa2a54443eff1950ba5ddc6b6ccda0d9c84a364276a62f969bdf2a390650848", size = 2182573, upload-time = "2026-05-06T13:38:45.04Z" }, + { url = "https://files.pythonhosted.org/packages/37/b3/7c40325848ba78247f2812dcf9c7274e38cd801820ca6dd9fe63bcfb0eb4/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:18e5ceec2ab67e6d5f1a9085e5a24c9c4e2ac4545730bfe668680bca05e555f3", size = 2317139, upload-time = "2026-05-06T13:37:15.539Z" }, + { url = "https://files.pythonhosted.org/packages/d9/37/f913f81a657c865b75da6c0dbed79876073c2a43b5bd9edbe8da785e4d49/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a0f62d0a58f4e7da165457e995725421e0064f2255d8eccebc49f41bbc23b109", size = 2360433, upload-time = "2026-05-06T13:37:30.099Z" }, + { url = "https://files.pythonhosted.org/packages/c4/67/6acaa1be2567f9256b056d8477158cac7240813956ce86e49deae8e173b4/pydantic_core-2.46.4-cp311-cp311-win32.whl", hash = "sha256:041bde0a48fd37cf71cab1c9d56d3e8625a3793fef1f7dd232b3ff37e978ecda", size = 1985513, upload-time = "2026-05-06T13:38:15.669Z" }, + { url = "https://files.pythonhosted.org/packages/aa/e6/c505f83dfeda9a2e5c995cfd872949e4d05e12f7feb3dca72f633daefa94/pydantic_core-2.46.4-cp311-cp311-win_amd64.whl", hash = "sha256:6f2eeda33a839975441c86a4119e1383c50b47faf0cbb5176985565c6bb02c33", size = 2071114, upload-time = "2026-05-06T13:40:35.416Z" }, + { url = "https://files.pythonhosted.org/packages/0f/da/7a263a96d965d9d0df5e8de8a475f33495451117035b09acb110288c381f/pydantic_core-2.46.4-cp311-cp311-win_arm64.whl", hash = "sha256:14f4c5d6db102bd796a627bbb3a17b4cf4574b9ae861d8b7c9a9661c6dd3362d", size = 2044298, upload-time = "2026-05-06T13:38:29.754Z" }, + { url = "https://files.pythonhosted.org/packages/ce/8c/af022f0af448d7747c5154288d46b5f2bc5f17366eaa0e23e9aa04d59f3b/pydantic_core-2.46.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:3245406455a5d98187ec35530fd772b1d799b26667980872c8d4614991e2c4a2", size = 2106158, upload-time = "2026-05-06T13:38:57.215Z" }, + { url = "https://files.pythonhosted.org/packages/19/95/6195171e385007300f0f5574592e467c568becce2d937a0b6804f218bc49/pydantic_core-2.46.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:962ccbab7b642487b1d8b7df90ef677e03134cf1fd8880bf698649b22a69371f", size = 1951724, upload-time = "2026-05-06T13:37:02.697Z" }, + { url = "https://files.pythonhosted.org/packages/8e/bc/f47d1ff9cbb1620e1b5b697eef06010035735f07820180e74178226b27b3/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8233f2947cf85404441fd7e0085f53b10c93e0ee78611099b5c7237e36aacbf7", size = 1975742, upload-time = "2026-05-06T13:37:09.448Z" }, + { url = "https://files.pythonhosted.org/packages/5b/11/9b9a5b0306345664a2da6410877af6e8082481b5884b3ddd78d47c6013ce/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3a233125ac121aa3ffba9a2b59edfc4a985a76092dc8279586ab4b71390875e7", size = 2052418, upload-time = "2026-05-06T13:37:38.234Z" }, + { url = "https://files.pythonhosted.org/packages/f1/b7/a65fec226f5d78fc39f4a13c4cc0c768c22b113438f60c14adc9d2865038/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b712b53160b79a5850310b912a5ef8e57e56947c8ad690c227f5c9d7e561712", size = 2232274, upload-time = "2026-05-06T13:38:27.753Z" }, + { url = "https://files.pythonhosted.org/packages/68/f0/92039db98b907ef49269a8271f67db9cb78ae2fc68062ef7e4e77adb5f61/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9401557acd873c3a7f3eb9383edef8ac4968f9510e340f4808d427e75667e7b4", size = 2309940, upload-time = "2026-05-06T13:38:05.353Z" }, + { url = "https://files.pythonhosted.org/packages/5f/97/2aab507d3d00ca626e8e57c1eac6a79e4e5fbcc63eb99733ff55d1717f65/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:926c9541b14b12b1681dca8a0b75feb510b06c6341b70a8e500c2fdcff837cce", size = 2094516, upload-time = "2026-05-06T13:39:10.577Z" }, + { url = "https://files.pythonhosted.org/packages/22/37/a8aca44d40d737dde2bc05b3c6c07dff0de07ce6f82e9f3167aeaf4d5dea/pydantic_core-2.46.4-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:56cb4851bcaf3d117eddcef4fe66afd750a50274b0da8e22be256d10e5611987", size = 2136854, upload-time = "2026-05-06T13:40:22.59Z" }, + { url = "https://files.pythonhosted.org/packages/24/99/fcef1b79238c06a8cbec70819ac722ba76e02bc8ada9b0fd66eba40da01b/pydantic_core-2.46.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c68fcd102d71ea85c5b2dfac3f4f8476eff42a9e078fd5faefff6d145063536b", size = 2180306, upload-time = "2026-05-06T13:40:10.666Z" }, + { url = "https://files.pythonhosted.org/packages/ae/6c/fc44000918855b42779d007ae63b0532794739027b2f417321cddbc44f6a/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b2f69dec1725e79a012d920df1707de5caf7ed5e08f3be4435e25803efc47458", size = 2190044, upload-time = "2026-05-06T13:40:43.231Z" }, + { url = "https://files.pythonhosted.org/packages/6b/65/d9cadc9f1920d7a127ad2edba16c1db7916e59719285cd6c94600b0080ba/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:8d0820e8192167f80d88d64038e609c31452eeca865b4e1d9950a27a4609b00b", size = 2329133, upload-time = "2026-05-06T13:39:57.365Z" }, + { url = "https://files.pythonhosted.org/packages/d0/cf/c873d91679f3a30bcf5e7ac280ce5573483e72295307685120d0d5ad3416/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fbdb89b3e1c94a30cc5edfce477c6e6a5dc4d8f84665b455c27582f211a1c72c", size = 2374464, upload-time = "2026-05-06T13:38:06.976Z" }, + { url = "https://files.pythonhosted.org/packages/47/bd/6f2fc8188f31bf10590f1e98e7b306336161fac930a8c514cd7bd828c7dc/pydantic_core-2.46.4-cp312-cp312-win32.whl", hash = "sha256:9aa768456404a8bf48a4406685ac2bec8e72b62c69313734fa3b73cf33b3a894", size = 1974823, upload-time = "2026-05-06T13:40:47.985Z" }, + { url = "https://files.pythonhosted.org/packages/40/8c/985c1d41ea1107c2534abd9870e4ed5c8e7669b5c308297835c001e7a1c4/pydantic_core-2.46.4-cp312-cp312-win_amd64.whl", hash = "sha256:e9c26f834c65f5752f3f06cb08cb86a913ceb7274d0db6e267808a708b46bc89", size = 2072919, upload-time = "2026-05-06T13:39:21.153Z" }, + { url = "https://files.pythonhosted.org/packages/c4/ba/f463d006e0c47373ca7ec5e1a261c59dc01ef4d62b2657af925fb0deee3a/pydantic_core-2.46.4-cp312-cp312-win_arm64.whl", hash = "sha256:4fc73cb559bdb54b1134a706a2802a4cddd27a0633f5abb7e53056268751ac6a", size = 2027604, upload-time = "2026-05-06T13:39:03.753Z" }, + { url = "https://files.pythonhosted.org/packages/51/a2/5d30b469c5267a17b39dec53208222f76a8d351dfac4af661888c5aee77d/pydantic_core-2.46.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:5d5902252db0d3cedf8d4a1bc68f70eeb430f7e4c7104c8c476753519b423008", size = 2106306, upload-time = "2026-05-06T13:37:48.029Z" }, + { url = "https://files.pythonhosted.org/packages/c1/81/4fa520eaffa8bd7d1525e644cd6d39e7d60b1592bc5b516693c7340b50f1/pydantic_core-2.46.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c94f0688e7b8d0a67abf40e57a7eaaecd17cc9586706a31b76c031f63df052b4", size = 1951906, upload-time = "2026-05-06T13:37:17.012Z" }, + { url = "https://files.pythonhosted.org/packages/03/d5/fd02da45b659668b05923b17ba3a0100a0a3d5541e3bd8fcc4ecb711309e/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f027324c56cd5406ca49c124b0db10e56c69064fec039acc571c29020cc87c76", size = 1976802, upload-time = "2026-05-06T13:37:35.113Z" }, + { url = "https://files.pythonhosted.org/packages/21/f2/95727e1368be3d3ed485eaab7adbd7dda408f33f7a36e8b48e0144002b91/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e739fee756ba1010f8bcccb534252e85a35fe45ae92c295a06059ce58b74ccd3", size = 2052446, upload-time = "2026-05-06T13:37:12.313Z" }, + { url = "https://files.pythonhosted.org/packages/9c/86/5d99feea3f77c7234b8718075b23db11532773c1a0dbd9b9490215dc2eeb/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d56801be94b86a9da183e5f3766e6310752b99ff647e38b09a9500d88e46e76", size = 2232757, upload-time = "2026-05-06T13:39:01.149Z" }, + { url = "https://files.pythonhosted.org/packages/d2/3a/508ac615935ef7588cf6d9e9b91309fdc2da751af865e02a9098de88258c/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2412e734dcb48da14d4e4006b82b46b74f2518b8a26ee7e58c6844a6cd6d03c4", size = 2309275, upload-time = "2026-05-06T13:37:41.406Z" }, + { url = "https://files.pythonhosted.org/packages/07/f8/41db9de19d7987d6b04715a02b3b40aea467000275d9d758ffaa31af7d50/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9551187363ffc0de2a00b2e47c25aeaeb1020b69b668762966df15fc5659dd5a", size = 2094467, upload-time = "2026-05-06T13:39:18.847Z" }, + { url = "https://files.pythonhosted.org/packages/2c/e2/f35033184cb11d0052daf4416e8e10a502ea2ac006fc4f459aee872727d1/pydantic_core-2.46.4-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:0186750b482eefa11d7f435892b09c5c606193ef3375bcf94aa00ae6bfb66262", size = 2134417, upload-time = "2026-05-06T13:40:17.944Z" }, + { url = "https://files.pythonhosted.org/packages/7e/7b/6ceeb1cc90e193862f444ebe373d8fdf613f0a82572dde03fb10734c6c71/pydantic_core-2.46.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5855698a4856556d86e8e6cd8434bc3ac0314ee8e12089ae0e143f64c6256e4e", size = 2179782, upload-time = "2026-05-06T13:40:32.618Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f2/c8d7773ede6af08036423a00ae0ceffce266c3c52a096c435d68c896083f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:cbaf13819775b7f769bf4a1f066cb6df7a28d4480081a589828ef190226881cd", size = 2188782, upload-time = "2026-05-06T13:36:51.018Z" }, + { url = "https://files.pythonhosted.org/packages/59/31/0c864784e31f09f05cdd87606f08923b9c9e7f6e51dd27f20f62f975ce9f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:633147d34cf4550417f12e2b1a0383973bdf5cdfde212cb09e9a581cf10820be", size = 2328334, upload-time = "2026-05-06T13:40:37.764Z" }, + { url = "https://files.pythonhosted.org/packages/c2/eb/4f6c8a41efa30baa755590f4141abf3a8c370fab610915733e74134a7270/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:82cf5301172168103724d49a1444d3378cb20cdee30b116a1bd6031236298a5d", size = 2372986, upload-time = "2026-05-06T13:39:34.152Z" }, + { url = "https://files.pythonhosted.org/packages/5b/24/b375a480d53113860c299764bfe9f349a3dc9108b3adc0d7f0d786492ebf/pydantic_core-2.46.4-cp313-cp313-win32.whl", hash = "sha256:9fa8ae11da9e2b3126c6426f147e0fba88d96d65921799bb30c6abd1cb2c97fb", size = 1973693, upload-time = "2026-05-06T13:37:55.072Z" }, + { url = "https://files.pythonhosted.org/packages/7e/e8/cff247591966f2d22ec8c003cd7587e27b7ba7b81ab2fb888e3ab75dc285/pydantic_core-2.46.4-cp313-cp313-win_amd64.whl", hash = "sha256:6b3ace8194b0e5204818c92802dcdca7fc6d88aabbb799d7c795540d9cd6d292", size = 2071819, upload-time = "2026-05-06T13:38:49.139Z" }, + { url = "https://files.pythonhosted.org/packages/c6/1a/f4aee670d5670e9e148e0c82c7db98d780be566c6e6a97ee8035528ca0b3/pydantic_core-2.46.4-cp313-cp313-win_arm64.whl", hash = "sha256:184c081504d17f1c1066e430e117142b2c77d9448a97f7b65c6ac9fd9aee238d", size = 2027411, upload-time = "2026-05-06T13:40:45.796Z" }, + { url = "https://files.pythonhosted.org/packages/8d/74/228a26ddad29c6672b805d9fd78e8d251cd04004fa7eed0e622096cd0250/pydantic_core-2.46.4-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:428e04521a40150c85216fc8b85e8d39fece235a9cf5e383761238c7fa9b96fb", size = 2102079, upload-time = "2026-05-06T13:38:41.019Z" }, + { url = "https://files.pythonhosted.org/packages/ad/1f/8970b150a4b4365623ae00fc88603491f763c627311ae8031e3111356d6e/pydantic_core-2.46.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23ace664830ee0bfe014a0c7bc248b1f7f25ed7ad103852c317624a1083af462", size = 1952179, upload-time = "2026-05-06T13:36:59.812Z" }, + { url = "https://files.pythonhosted.org/packages/95/30/5211a831ae054928054b2f79731661087a2bc5c01e825c672b3a4a8f1b3e/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce5c1d2a8b27468f433ca974829c44060b8097eedc39933e3c206a90ee49c4a9", size = 1978926, upload-time = "2026-05-06T13:37:39.933Z" }, + { url = "https://files.pythonhosted.org/packages/57/e9/689668733b1eb67adeef047db3c2e8788fcf65a7fd9c9e2b46b7744fe245/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7283d57845ecf5a163403eb0702dfc220cc4fbdd18919cb5ccea4f95ee1cdab4", size = 2046785, upload-time = "2026-05-06T13:38:01.995Z" }, + { url = "https://files.pythonhosted.org/packages/60/d9/6715260422ff50a2109878fd24d948a6c3446bb2664f34ee78cd972b3acd/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8daafc69c93ee8a0204506a3b6b30f586ef54028f52aeeeb5c4cfc5184fd5914", size = 2228733, upload-time = "2026-05-06T13:40:50.371Z" }, + { url = "https://files.pythonhosted.org/packages/18/ae/fdb2f64316afca925640f8e70bb1a564b0ec2721c1389e25b8eb4bf9a299/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd2213145bcc2ba85884d0ac63d222fece9209678f77b9b4d76f054c561adb28", size = 2307534, upload-time = "2026-05-06T13:37:21.531Z" }, + { url = "https://files.pythonhosted.org/packages/89/1d/8eff589b45bb8190a9d12c49cfad0f176a5cbd1534908a6b5125e2886239/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a5f930472650a82629163023e630d160863fce524c616f4e5186e5de9d9a49b", size = 2099732, upload-time = "2026-05-06T13:39:31.942Z" }, + { url = "https://files.pythonhosted.org/packages/06/d5/ee5a3366637fee41dee51a1fc91562dcf12ddbc68fda34e6b253da2324bb/pydantic_core-2.46.4-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:c1b3f518abeca3aa13c712fd202306e145abf59a18b094a6bafb2d2bbf59192c", size = 2129627, upload-time = "2026-05-06T13:37:25.033Z" }, + { url = "https://files.pythonhosted.org/packages/94/33/2414be571d2c6a6c4d08be21f9292b6d3fdb08949a97b6dfe985017821db/pydantic_core-2.46.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a7dd0b3ee80d90150e3495a3a13ac34dbcbfd4f012996a6a1d8900e91b5c0fb", size = 2179141, upload-time = "2026-05-06T13:37:14.046Z" }, + { url = "https://files.pythonhosted.org/packages/7b/79/7daa95be995be0eecc4cf75064cb33f9bbbfe3fe0158caf2f0d4a996a5c7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:3fb702cd90b0446a3a1c5e470bfa0dd23c0233b676a9099ddcc964fa6ca13898", size = 2184325, upload-time = "2026-05-06T13:36:53.615Z" }, + { url = "https://files.pythonhosted.org/packages/9f/cb/d0a382f5c0de8a222dc61c65348e0ce831b1f68e0a018450d31c2cace3a5/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b8458003118a712e66286df6a707db01c52c0f52f7db8e4a38f0da1d3b94fc4e", size = 2323990, upload-time = "2026-05-06T13:40:29.971Z" }, + { url = "https://files.pythonhosted.org/packages/05/db/d9ba624cc4a5aced1598e88c04fdbd8310c8a69b9d38b9a3d39ce3a61ed7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:372429a130e469c9cd698925ce5fc50940b7a1336b0d82038e63d5bbc4edc519", size = 2369978, upload-time = "2026-05-06T13:37:23.027Z" }, + { url = "https://files.pythonhosted.org/packages/f2/20/d15df15ba918c423461905802bfd2981c3af0bfa0e40d05e13edbfa48bc3/pydantic_core-2.46.4-cp314-cp314-win32.whl", hash = "sha256:85bb3611ff1802f3ee7fdd7dbff26b56f343fb432d57a4728fdd49b6ef35e2f4", size = 1966354, upload-time = "2026-05-06T13:38:03.499Z" }, + { url = "https://files.pythonhosted.org/packages/fc/b6/6b8de4c0a7d7ab3004c439c80c5c1e0a3e8d78bbae19379b01960383d9e5/pydantic_core-2.46.4-cp314-cp314-win_amd64.whl", hash = "sha256:811ff8e9c313ab425368bcbb36e5c4ebd7108c2bbf4e4089cfbb0b01eff63fac", size = 2072238, upload-time = "2026-05-06T13:39:40.807Z" }, + { url = "https://files.pythonhosted.org/packages/32/36/51eb763beec1f4cf59b1db243a7dcc39cbb41230f050a09b9d69faaf0a48/pydantic_core-2.46.4-cp314-cp314-win_arm64.whl", hash = "sha256:bfec22eab3c8cc2ceec0248aec886624116dc079afa027ecc8ad4a7e62010f8a", size = 2018251, upload-time = "2026-05-06T13:37:26.72Z" }, + { url = "https://files.pythonhosted.org/packages/e8/91/855af51d625b23aa987116a19e231d2aaef9c4a415273ddc189b79a45fee/pydantic_core-2.46.4-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:af8244b2bef6aaad6d92cda81372de7f8c8d36c9f0c3ea36e827c60e7d9467a0", size = 2099593, upload-time = "2026-05-06T13:39:47.682Z" }, + { url = "https://files.pythonhosted.org/packages/fb/1b/8784a54c65edb5f49f0a14d6977cf1b209bba85a4c77445b255c2de58ab3/pydantic_core-2.46.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a4330cdbc57162e4b3aa303f588ba752257694c9c9be3e7ebb11b4aca659b5d", size = 1935226, upload-time = "2026-05-06T13:40:40.428Z" }, + { url = "https://files.pythonhosted.org/packages/e8/e7/1955d28d1afc56dd4b3ad7cc0cf39df1b9852964cf16e5d13912756d6d6b/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29c61fc04a3d840155ff08e475a04809278972fe6aef51e2720554e96367e34b", size = 1974605, upload-time = "2026-05-06T13:37:32.029Z" }, + { url = "https://files.pythonhosted.org/packages/93/e2/3fedbf0ba7a22850e6e9fd78117f1c0f10f950182344d8a6c535d468fdd8/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c50f2528cf200c5eed56faf3f4e22fcd5f38c157a8b78576e6ba3168ec35f000", size = 2030777, upload-time = "2026-05-06T13:38:55.239Z" }, + { url = "https://files.pythonhosted.org/packages/f8/61/46be275fcaaba0b4f5b9669dd852267ce1ff616592dccf7a7845588df091/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0cbe8b01f948de4286c74cdd6c667aceb38f5c1e26f0693b3983d9d74887c65e", size = 2236641, upload-time = "2026-05-06T13:37:08.096Z" }, + { url = "https://files.pythonhosted.org/packages/60/db/12e93e46a8bac9988be3c016860f83293daea8c716c029c9ace279036f2f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:617d7e2ca7dcb8c5cf6bcb8c59b8832c94b36196bbf1cbd1bfb56ed341905edd", size = 2286404, upload-time = "2026-05-06T13:40:20.221Z" }, + { url = "https://files.pythonhosted.org/packages/e2/4a/4d8b19008f38d31c53b8219cfedc2e3d5de5fe99d90076b7e767de29274f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7027560ee92211647d0d34e3f7cd6f50da56399d26a9c8ad0da286d3869a53f3", size = 2109219, upload-time = "2026-05-06T13:38:12.153Z" }, + { url = "https://files.pythonhosted.org/packages/88/70/3cbc40978fefb7bb09c6708d40d4ad1a5d70fd7213c3d17f971de868ec1f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:f99626688942fb746e545232e7726926f3be91b5975f8b55327665fafda991c7", size = 2110594, upload-time = "2026-05-06T13:40:02.971Z" }, + { url = "https://files.pythonhosted.org/packages/9d/20/b8d36736216e29491125531685b2f9e61aa5b4b2599893f8268551da3338/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fc3e9034a63de20e15e8ade85358bc6efc614008cab72898b4b4952bea0509ff", size = 2159542, upload-time = "2026-05-06T13:39:27.506Z" }, + { url = "https://files.pythonhosted.org/packages/1d/a2/367df868eb584dacf6bf82a389272406d7178e301c4ac82545ab98bc2dd9/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:97e7cf2be5c77b7d1a9713a05605d49460d02c6078d38d8bef3cbe323c548424", size = 2168146, upload-time = "2026-05-06T13:38:31.93Z" }, + { url = "https://files.pythonhosted.org/packages/c1/b8/4460f77f7e201893f649a29ab355dddd3beee8a97bcb1a320db414f9a06e/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:3bf92c5d0e00fefaab325a4d27828fe6b6e2a21848686b5b60d2d9eeb09d76c6", size = 2306309, upload-time = "2026-05-06T13:37:44.717Z" }, + { url = "https://files.pythonhosted.org/packages/64/c4/be2639293acd87dc8ddbcec41a73cee9b2ebf996fe6d892a1a74e88ad3f7/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:3ecbc122d18468d06ca279dc26a8c2e2d5acb10943bb35e36ae92096dc3b5565", size = 2369736, upload-time = "2026-05-06T13:37:05.645Z" }, + { url = "https://files.pythonhosted.org/packages/30/a6/9f9f380dbb301f67023bf8f707aaa75daadf84f7152d95c410fd7e81d994/pydantic_core-2.46.4-cp314-cp314t-win32.whl", hash = "sha256:e846ae7835bf0703ae43f534ab79a867146dadd59dc9ca5c8b53d5c8f7c9ef02", size = 1955575, upload-time = "2026-05-06T13:38:51.116Z" }, + { url = "https://files.pythonhosted.org/packages/40/1f/f1eb9eb350e795d1af8586289746f5c5677d16043040d63710e22abc43c9/pydantic_core-2.46.4-cp314-cp314t-win_amd64.whl", hash = "sha256:2108ba5c1c1eca18030634489dc544844144ee36357f2f9f780b93e7ddbb44b5", size = 2051624, upload-time = "2026-05-06T13:38:21.672Z" }, + { url = "https://files.pythonhosted.org/packages/f6/d2/42dd53d0a85c27606f316d3aa5d2869c4e8470a5ed6dec30e4a1abe19192/pydantic_core-2.46.4-cp314-cp314t-win_arm64.whl", hash = "sha256:4fcbe087dbc2068af7eda3aa87634eba216dbda64d1ae73c8684b621d33f6596", size = 2017325, upload-time = "2026-05-06T13:40:52.723Z" }, + { url = "https://files.pythonhosted.org/packages/ee/a4/73995fd4ebbb46ba0ee51e6fa049b8f02c40daebb762208feda8a6b7894d/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:14d4edf427bdcf950a8a02d7cb44a08614388dd6e1bdcbf4f67504fa7887da9c", size = 2111589, upload-time = "2026-05-06T13:37:10.817Z" }, + { url = "https://files.pythonhosted.org/packages/fb/7f/f37d3a5e8bfcc2e403f5c57a730f2d815693fb42119e8ea48b3789335af1/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:0ce40cd7b21210e99342afafbd4d0f76d784eb5b1d60f3bdc566be4983c6c73b", size = 1944552, upload-time = "2026-05-06T13:36:56.717Z" }, + { url = "https://files.pythonhosted.org/packages/15/3c/d7eb777b3ff43e8433a4efb39a17aa8fd98a4ee8561a24a67ef5db07b2d6/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:90884113d8b48f760e9587002789ddd741e76ab9f89518cd1e43b1f1a52ec44b", size = 1982984, upload-time = "2026-05-06T13:39:06.207Z" }, + { url = "https://files.pythonhosted.org/packages/63/87/70b9f40170a81afd55ca26c9b2acb25c20d64bcfbf888fafecb3ba077d4c/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:66ce7632c22d837c95301830e111ad0128a32b8207533b60896a96c4915192ea", size = 2138417, upload-time = "2026-05-06T13:39:45.476Z" }, + { url = "https://files.pythonhosted.org/packages/9d/1d/8987ad40f65ae1432753072f214fb5c74fe47ffbd0698bb9cbbb585664f8/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:1d8ba486450b14f3b1d63bc521d410ec7565e52f887b9fb671791886436a42f7", size = 2095527, upload-time = "2026-05-06T13:39:52.283Z" }, + { url = "https://files.pythonhosted.org/packages/64/d3/84c282a7eee1d3ac4c0377546ef5a1ea436ce26840d9ac3b7ed54a377507/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:3009f12e4e90b7f88b4f9adb1b0c4a3d58fe7820f3238c190047209d148026df", size = 1936024, upload-time = "2026-05-06T13:40:15.671Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ca/eac61596cdeb4d7e174d3dc0bd8a6238f14f75f97a24e7b7db4c7e7340a0/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad785e92e6dc634c21555edc8bd6b64957ab844541bcb96a1366c202951ae526", size = 1990696, upload-time = "2026-05-06T13:38:34.717Z" }, + { url = "https://files.pythonhosted.org/packages/fa/c3/7c8b240552251faf6b3a957db200fcfbbcec36763c050428b601e0c9b83b/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00c603d540afdd6b80eb39f078f33ebd46211f02f33e34a32d9f053bba711de0", size = 2147590, upload-time = "2026-05-06T13:39:29.883Z" }, + { url = "https://files.pythonhosted.org/packages/11/cb/428de0385b6c8d44b716feba566abfacfbd23ee3c4439faa789a1456242f/pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:0c563b08bca408dc7f65f700633d8442fffb2421fc47b8101377e9fd65051ff0", size = 2112782, upload-time = "2026-05-06T13:37:04.016Z" }, + { url = "https://files.pythonhosted.org/packages/0b/b5/6a17bdadd0fc1f170adfd05a20d37c832f52b117b4d9131da1f41bb097ce/pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:db06ffe51636ffe9ca531fe9023dd64bdd794be8754cb5df57c5498ae5b518a7", size = 1952146, upload-time = "2026-05-06T13:39:43.092Z" }, + { url = "https://files.pythonhosted.org/packages/2a/dc/03734d80e362cd43ef65428e9de77c730ce7f2f11c60d2b1e1b39f0fbf99/pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:133878133d271ade3d41d1bfb2a45ec38dbdbda40bc065921c6b04e4630127e2", size = 2134492, upload-time = "2026-05-06T13:36:58.124Z" }, + { url = "https://files.pythonhosted.org/packages/de/df/5e5ffc085ed07cc22d298134d3d911c63e91f6a0eb91fe646750a3209910/pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9bc519fbf2b7578398853d815009ae5e4d4603d12f4e3f91da8c06852d3da3e9", size = 2156604, upload-time = "2026-05-06T13:37:49.88Z" }, + { url = "https://files.pythonhosted.org/packages/81/44/6e112a4253e56f5705467cbab7ab5e91ee7398ba3d56d358635958893d3e/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:c7a7bd4e39e8e4c12c39cd480356842b6a8a06e41b23a55a5e3e191718838ddf", size = 2183828, upload-time = "2026-05-06T13:37:43.053Z" }, + { url = "https://files.pythonhosted.org/packages/ac/ad/5565071e937d8e752842ac241463944c9eb14c87e2d269f2658a5bd05e98/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:d396ec2b979760aaf3218e76c24e65bd0aca24983298653b3a9d7a45f9e47b30", size = 2310000, upload-time = "2026-05-06T13:37:56.694Z" }, + { url = "https://files.pythonhosted.org/packages/4f/c3/66883a5cec183e7fba4d024b4cbbe61851a63750ef606b0afecc46d1f2bf/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:86e1a4418c6cd97d60c95c71164158eaf7324fae7b0923264016baa993eba6fc", size = 2361286, upload-time = "2026-05-06T13:40:05.667Z" }, + { 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" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" }, + { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" }, + { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" }, + { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" }, + { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" }, + { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" }, + { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" }, + { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" }, + { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" }, + { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, + { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, + { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, + { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, + { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, + { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, + { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, + { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, + { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, + { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, + { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, + { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, + { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, + { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, + { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, +] + +[[package]] +name = "ruff" +version = "0.15.22" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3a/06/ae069393fc66e8ff33036d4b368003833bf6e88ccf182e17e7a2f1c754fd/ruff-0.15.22.tar.gz", hash = "sha256:3f15175b1fb580126f58285a5dae6b2ea89000136d980c64499211f116b54809", size = 4785063, upload-time = "2026-07-16T15:14:13.244Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/23/18/ee54b7ae1e121be7a28ea6da4b67564ebb0530e183a54415ab7e3bcd2c4e/ruff-0.15.22-py3-none-linux_armv6l.whl", hash = "sha256:44423e73493737f5e7c5b41d475483898ff37afcdae38bc3da5085e29af1c2d8", size = 10781258, upload-time = "2026-07-16T15:13:19.452Z" }, + { url = "https://files.pythonhosted.org/packages/2f/d2/2520cb14761ddbeaf57642a76942fc36adcbdbe53b4532241995f6fc485c/ruff-0.15.22-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b82c6482946e9eda7ff2e091d25b8bad3f718684e1916d41bd56873cee05b697", size = 10999477, upload-time = "2026-07-16T15:13:23.318Z" }, + { url = "https://files.pythonhosted.org/packages/c9/10/74e53572aa758dfaa678c2a2646b5c5515d884b7ca56be4d2ce03ca4b560/ruff-0.15.22-py3-none-macosx_11_0_arm64.whl", hash = "sha256:11c1c715af53a09f714e011106bffc419751ec8232fcb5da42173284ea3fec6f", size = 10466716, upload-time = "2026-07-16T15:13:26.162Z" }, + { url = "https://files.pythonhosted.org/packages/1e/cc/44eaaf0844e028182f2d0a8f2190d0f359159aed0a9e5ab861d892f1ae2a/ruff-0.15.22-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:742a29cf29bddb7c8327895d6a10e0e6c5b38a96dd407af9b5d0857f809c0576", size = 10892644, upload-time = "2026-07-16T15:13:29.229Z" }, + { url = "https://files.pythonhosted.org/packages/9f/21/8edf559014d2b0f82beea19cfb713993ad802ccda16868769979c6090a84/ruff-0.15.22-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72af58b951b0ae395935ae79763dc349bc0eb706319d28f7a33ad2cfb3cfc178", size = 10576719, upload-time = "2026-07-16T15:13:32.35Z" }, + { url = "https://files.pythonhosted.org/packages/bf/1e/3a13abd392a3b50b62e5938a831f9ab6e588358cacad5c18545b716d2182/ruff-0.15.22-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:62d425005c1835eb24e2ee4161cb90e8db263415f4a71c8c72c33abaa6c0c224", size = 11376494, upload-time = "2026-07-16T15:13:35.958Z" }, + { url = "https://files.pythonhosted.org/packages/bf/3e/422d3d95bcf04dd78e1aeac22184d4f9a8fb2c01865d39d44618484a0317/ruff-0.15.22-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e8b9b3f8779a4f08c969defc3c8c35abffaa757e601ed5ae66d6d1db6519969a", size = 12208370, upload-time = "2026-07-16T15:13:39.185Z" }, + { url = "https://files.pythonhosted.org/packages/1e/91/5d065a0e0a02bf4813f5119ad278462eed081d2b832eb7c021ade0ec9e65/ruff-0.15.22-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1e0dd1b2e4d3d585f897a0d137cbf4eaf6223bef4e8ce34d6bb12556c5f9249e", size = 11581098, upload-time = "2026-07-16T15:13:42.132Z" }, + { url = "https://files.pythonhosted.org/packages/f6/f9/a0d4871d12fae702eb1f41b686caf05f1f8b124dc6db6f784f53d74918fa/ruff-0.15.22-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:365523eb91d9224e1bcb03b022fbf0facb8f9e23792a2c53d9d4b3924bdbdebb", size = 11399422, upload-time = "2026-07-16T15:13:45.2Z" }, + { url = "https://files.pythonhosted.org/packages/18/80/c843a5176cddbceb0b7e8dd41cf9993490796c1c469348d384f5a5c13c56/ruff-0.15.22-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:fabfd168afdf29fee5be98b831efa9683c94d7c5a3b58b9ce5a2e38444589a74", size = 11381683, upload-time = "2026-07-16T15:13:48.46Z" }, + { url = "https://files.pythonhosted.org/packages/d4/00/8485de0ae92239438a36cfc51350db9b9e85c9ebdfaea91b18e422706662/ruff-0.15.22-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:225dbf095a87f1d9f90f5fd7924d2613ee452a75a4308c63a8f50f761787aa7c", size = 10850295, upload-time = "2026-07-16T15:13:51.655Z" }, + { url = "https://files.pythonhosted.org/packages/fa/91/24977ec2ec72eaf15e4394ace2959fdff2dd1e14f03e005e838023407169/ruff-0.15.22-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:1877d63b9d24ed278744f1523fd11b85540566d54641f97c566d7d9dc5ca5296", size = 10579640, upload-time = "2026-07-16T15:13:54.79Z" }, + { url = "https://files.pythonhosted.org/packages/9c/47/9b51216951974df1f263ac19da550d34252e0ed7218c25f10c5ef9ed7517/ruff-0.15.22-py3-none-musllinux_1_2_i686.whl", hash = "sha256:a1606c510bd7215680d32efab38965f7cdec3ef69f5170a3f4791404ffdd5262", size = 11105077, upload-time = "2026-07-16T15:13:57.915Z" }, + { url = "https://files.pythonhosted.org/packages/c2/47/20e9d4a3b8016778acea5fc32bb50d35d207500a17ddb529ffa6996feef8/ruff-0.15.22-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:630479b18625f5ffc373f77603a22a9f8ac0acd7ff0501178b5db28ec71e9c64", size = 11490980, upload-time = "2026-07-16T15:14:01.032Z" }, + { url = "https://files.pythonhosted.org/packages/4d/76/3f72d8fc38c1cb77b38c56a70da9d0c17700cc1cc50f9649c9d3c8f5ba71/ruff-0.15.22-py3-none-win32.whl", hash = "sha256:e5ba0e4a13fd14abbed2a77b517a3911290c6c6c59ef67784328d1668fab76cf", size = 10789165, upload-time = "2026-07-16T15:14:04.16Z" }, + { url = "https://files.pythonhosted.org/packages/cb/46/4965251734c2b6fcdca1b1b187d20bcac3af0ee5b083b89c910bb961ce3a/ruff-0.15.22-py3-none-win_amd64.whl", hash = "sha256:9be63ba1eb936acd2d1342fb8337c356353706fce233b2a15a09a97037e6acde", size = 11938297, upload-time = "2026-07-16T15:14:07.316Z" }, + { url = "https://files.pythonhosted.org/packages/57/c9/e69b1ff4c8b69093ef08b8919ab767af0569666865b39c30a8795d88d3c6/ruff-0.15.22-py3-none-win_arm64.whl", hash = "sha256:e1168075b72158510839f250027659cdd78476f40507dd517892304c41318661", size = 11298172, upload-time = "2026-07-16T15:14:10.51Z" }, +] + +[[package]] +name = "semantido" +version = "0.5.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "sqlalchemy" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f9/a1/796a014b9b66159e3c63e499af7b583ef571574d96e829cc88cd8a461706/semantido-0.5.2.tar.gz", hash = "sha256:49b0cef65019e64a228a49aaa3c2d65d3457949d291c57b1ca7f2c135fd0f682", size = 135557, upload-time = "2026-08-02T16:47:23.454Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dd/2f/48962000957959e0c7ee827fd1ca19bc81f3e30a42cd77a95346a4fb6059/semantido-0.5.2-py3-none-any.whl", hash = "sha256:cbc3573d32e69c3c1eb776fa37362ad9dee8d94a37f40142e6ec1e4817b3c864", size = 66209, upload-time = "2026-08-02T16:47:24.582Z" }, +] + +[[package]] +name = "sqlalchemy" +version = "2.0.51" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "greenlet", marker = "platform_machine == 'AMD64' or platform_machine == 'WIN32' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'ppc64le' or platform_machine == 'win32' or platform_machine == 'x86_64'" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/02/f1/a7a892f18d4d224e6b26f706531eafccc41e37594d37d304786969ee13cb/sqlalchemy-2.0.51.tar.gz", hash = "sha256:804dccd8a4a6242c4e30ad961e540e18a588f6527202f2d6791b01845d59fdc9", size = 9912201, upload-time = "2026-06-15T15:41:20.012Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3a/69/a67c69e5f28fc9c99d6f7bd60bd50e91f2fed2423e3b30fb228fa00e51f3/sqlalchemy-2.0.51-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1aa10c0daee6705294d181daadaa793221e1a59ed55000a3fab1d42b088ce4ba", size = 2161838, upload-time = "2026-06-15T16:05:17.144Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a4/c8c22b8438bddc0a030157c6ec0f6ef97b3c38effa444bdab2a27af04090/sqlalchemy-2.0.51-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a5b2ed6d828f1f09bd812861f4f59ca3bc3803f9df871f4555187f0faf018604", size = 3319402, upload-time = "2026-06-15T16:10:40.002Z" }, + { url = "https://files.pythonhosted.org/packages/90/54/44012d32fd77d991256d2ff793ba3807c51d40cb27a85b4796224f6744df/sqlalchemy-2.0.51-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:436728ce18a80f6951a1e11cc6112c2ede9faf20766f1a26195a7c441ca12dbd", size = 3319675, upload-time = "2026-06-15T16:12:25.658Z" }, + { url = "https://files.pythonhosted.org/packages/29/a5/de0592acaf5906cd7430874392d6f7e8b4a7c8437610953ee2d1501c0b44/sqlalchemy-2.0.51-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:dc261707bf5739aea8a541593f3cc1d463c2701fb05fbcbba0ce031b69a21260", size = 3270777, upload-time = "2026-06-15T16:10:42.125Z" }, + { url = "https://files.pythonhosted.org/packages/cb/14/a44c90739c780b362238e4ac3cb19dd0ca40d13e6ddc5daa112166ddab4f/sqlalchemy-2.0.51-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a6d26094615306d116dd5e4a51b0304c99dd2356fc569eed6922a80a6bd3b265", size = 3293940, upload-time = "2026-06-15T16:12:27.156Z" }, + { url = "https://files.pythonhosted.org/packages/65/eb/fbd0f206a330e66f8c602a99c37c4e731f107faed62954b41b01f16dd9d9/sqlalchemy-2.0.51-cp311-cp311-win32.whl", hash = "sha256:ca8435d13829b92f4a97362d91975154a4015db3a2634154e1754e9a915e6b86", size = 2121183, upload-time = "2026-06-15T16:13:29.905Z" }, + { url = "https://files.pythonhosted.org/packages/ad/fd/005bf80f3cf6e5c62b5dd68616280f51cd012c60840fa74781b3ed7b1623/sqlalchemy-2.0.51-cp311-cp311-win_amd64.whl", hash = "sha256:4a011ea4510683319ce4ed274b56ee05194b39b6da9d09ca7a39388f0fa84dcc", size = 2145796, upload-time = "2026-06-15T16:13:31.283Z" }, + { url = "https://files.pythonhosted.org/packages/d5/70/e868bc5412acd101a8280f25c95f10eeae0771c4eb806b02491142810ee8/sqlalchemy-2.0.51-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d78702b26ba1c18b2d0fb2ea940ba7f17a9581b42e8361ff93920ebbee1235a", size = 2160291, upload-time = "2026-06-15T16:08:48.918Z" }, + { url = "https://files.pythonhosted.org/packages/e5/1c/71ee0f8a6b9d7316a1ccd30430b4c62b6c2e36adc96017a4e3a72dce49d6/sqlalchemy-2.0.51-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:581921d849d6e6f994d560389192955e80e2950e18fcdfe2ccea863e01158e6e", size = 3343835, upload-time = "2026-06-15T16:19:42.613Z" }, + { url = "https://files.pythonhosted.org/packages/2b/7c/7ab9f9aadc5944fdd06612484ed7918fe376ad871a5f50404dc1536e0194/sqlalchemy-2.0.51-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1d21ce524ab86c23046e992a5b81cb54c21079c6df6e78b8fc77d77cac70a6b9", size = 3358470, upload-time = "2026-06-15T16:26:38.011Z" }, + { url = "https://files.pythonhosted.org/packages/d0/7d/ff77169fee6186de145a7f2b87006c39638391130abbab2b1f63ac6ea583/sqlalchemy-2.0.51-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c5d98a2709840027f5a347c3af0a7c3d5f6c1ff93af2ca1c54494e23cba8f389", size = 3289874, upload-time = "2026-06-15T16:19:45.212Z" }, + { url = "https://files.pythonhosted.org/packages/6f/3b/6c505903710d781b55bc3141ee34a062bf9745a6b5bc7333305b9ed63b33/sqlalchemy-2.0.51-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1181256e0f16479691b5616d36375dc2620ad8332b25978763c3d206ad3f3f1d", size = 3321692, upload-time = "2026-06-15T16:26:39.747Z" }, + { url = "https://files.pythonhosted.org/packages/3c/b7/c5ffe50aa2f4d947c9250e1519d939260329a07fe6272edfccd784b3d007/sqlalchemy-2.0.51-cp312-cp312-win32.whl", hash = "sha256:9f380393be5abeb6815f68fd39271b95127173511b6706b0a630a9995d53f8f5", size = 2119674, upload-time = "2026-06-15T16:23:09.543Z" }, + { url = "https://files.pythonhosted.org/packages/25/dc/46a65916af68a06ef6b972c6050ba4c8f97070fe3fb33097d34229d9bef6/sqlalchemy-2.0.51-cp312-cp312-win_amd64.whl", hash = "sha256:2cf39aabdf48e87c1c2c2ed6d20d33ffa0733b3071ce9c5f66357947dd009080", size = 2146670, upload-time = "2026-06-15T16:23:11.048Z" }, + { url = "https://files.pythonhosted.org/packages/54/fe/a210d52fd1a90ecfae8a78e9d8b27e18d733d60818a8bf250ff690b75120/sqlalchemy-2.0.51-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7c2056838b6685b72fdb36c99996cf862753461a62f2e84f4196371d3b2d6a07", size = 2157184, upload-time = "2026-06-15T16:08:50.374Z" }, + { url = "https://files.pythonhosted.org/packages/17/6b/2dce8369b199cb855110e056032f94a9f66dacc2237d3d39c115a86eac56/sqlalchemy-2.0.51-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:483b11bd46bf35fc14c52faf338b04300c9e6ce554bce9b11be85bfec3bc3195", size = 3284735, upload-time = "2026-06-15T16:19:46.934Z" }, + { url = "https://files.pythonhosted.org/packages/53/ff/dbc495b8a14da840faffb353857a72d4190113cac33727906fb997047f0f/sqlalchemy-2.0.51-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1bed1ee8b01da6088210aa9412023326fb98a599ba502e6118308601dcbef77f", size = 3302756, upload-time = "2026-06-15T16:26:41.336Z" }, + { url = "https://files.pythonhosted.org/packages/cf/d5/fde8f4dddcf518ee15ab35a7c6a28acc32c8ba548d1d2aa451f96e6dbb0b/sqlalchemy-2.0.51-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:72ca54c952107ba5cd58854b67a5a6268631289d21651a1235396f3b98b47400", size = 3232055, upload-time = "2026-06-15T16:19:49.286Z" }, + { url = "https://files.pythonhosted.org/packages/67/d1/43d3a0ac955a58601c24fa23038b1c55ee3a1ec02c0f96ebb1eae2bcf614/sqlalchemy-2.0.51-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b3e693d15533a45cd5906f0589f9c35090bef6ef45bf1e8195c424aa0ae06a8d", size = 3269850, upload-time = "2026-06-15T16:26:43.017Z" }, + { url = "https://files.pythonhosted.org/packages/94/df/de669c7054cd47c4439ac34b1b2ee8b804a794791fbb10720e997a2c87c7/sqlalchemy-2.0.51-cp313-cp313-win32.whl", hash = "sha256:b93ab07b5292dbe7e6b8da89475275e7042744283921344b56105f3eeb0f828b", size = 2117721, upload-time = "2026-06-15T16:23:12.36Z" }, + { url = "https://files.pythonhosted.org/packages/d0/8a/403c51d064196bae20a0bc2476577f83a3f8dd299719a97417086b7f2ec5/sqlalchemy-2.0.51-cp313-cp313-win_amd64.whl", hash = "sha256:0f053118c30e53161857a953e4de667d90e274980dccbe5dd3829bbbeece72a5", size = 2143615, upload-time = "2026-06-15T16:23:13.906Z" }, + { url = "https://files.pythonhosted.org/packages/b1/49/a739be2e1d02a96a658eb71ab45d921c874249252358ad24a5bffdd02525/sqlalchemy-2.0.51-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:6ea306caaae6bd5afd0a46050003c88f6bf33227377a49298c498c3cb88ff491", size = 2158999, upload-time = "2026-06-15T16:08:51.759Z" }, + { url = "https://files.pythonhosted.org/packages/23/6b/2e0e38cf75c8780eca78d9b2e78164f8bcfd70125e5caa588ff5cbb9c9f4/sqlalchemy-2.0.51-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c45a496d6bc05dec41dcd4c3a2b183723f47473255c159cd80b503c8f246424d", size = 3282539, upload-time = "2026-06-15T16:19:51.065Z" }, + { url = "https://files.pythonhosted.org/packages/dd/a1/e77854cb5336fd37dc3c6ae3b71de242c98caac5725120be0b526b31cbd0/sqlalchemy-2.0.51-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4004ada0aafe8ae1991b2cd1d99c6d9146126e123bd6f883c260d974aa012e54", size = 3287545, upload-time = "2026-06-15T16:26:44.735Z" }, + { url = "https://files.pythonhosted.org/packages/f6/ab/9e17272fd4dac8df3b83c4fbe52b998a1c9d89a843c8c35ff29b74ff7364/sqlalchemy-2.0.51-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0f6bcad487aee1c638d707235682fc96f741de00663619881ab235400d03289e", size = 3230929, upload-time = "2026-06-15T16:19:52.625Z" }, + { url = "https://files.pythonhosted.org/packages/02/3c/52f408ea701781caee975606beccc48845f2aee8711ac29843d612c0306c/sqlalchemy-2.0.51-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:39a76529db6305693d8d4affa58ad5b5e2e18edd62daea628b29b97930b3513d", size = 3252888, upload-time = "2026-06-15T16:26:46.454Z" }, + { url = "https://files.pythonhosted.org/packages/24/16/3efd2ee6bc4ca4693a30a1dd17a91b606cae15d517d2a4746611d9b73ce8/sqlalchemy-2.0.51-cp314-cp314-win32.whl", hash = "sha256:08a204d8b5638717c26a24df18fcf40af45a6b22e35b70b1d62f0113c2e278e8", size = 2120551, upload-time = "2026-06-15T16:23:15.629Z" }, + { url = "https://files.pythonhosted.org/packages/7b/78/55b12e70f45bccc40d9e483925c065027b3b98ea4cbbdf6f8c2546feaf6c/sqlalchemy-2.0.51-cp314-cp314-win_amd64.whl", hash = "sha256:96747bfbadb055466e5b46d572618170046b45ce5a4879167f50d70a5319a499", size = 2146318, upload-time = "2026-06-15T16:23:17.108Z" }, + { url = "https://files.pythonhosted.org/packages/21/db/a9574ed40fed418924b1b1a3e54f47ee3963053b3d3d325a0d36b41f2c08/sqlalchemy-2.0.51-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e5ea1a213be1fcd5e49d9904c3b9939211ded90bc2a64e93f4c01963474285de", size = 2178920, upload-time = "2026-06-15T15:59:56.285Z" }, + { url = "https://files.pythonhosted.org/packages/bf/90/a1bb5c7cbba76b7bc1fbd586d0a5479a7bc9c27b4a8298f22ec9423b2bb3/sqlalchemy-2.0.51-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7c6b36ed71f41942bdcd2ad2522be46bfce09d5705be5640ecf19bbc7660e4b7", size = 3566534, upload-time = "2026-06-15T15:58:35.024Z" }, + { url = "https://files.pythonhosted.org/packages/15/4b/481f1fed30e0e9e8dd24aecbb49f29eb57fe7657ece5cf06ee9b84bb97d8/sqlalchemy-2.0.51-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0c2c62877097e1a0db401fba5cb4debee33265e5b2a55c4ccb489c02c53b4f72", size = 3535844, upload-time = "2026-06-15T16:02:43.973Z" }, + { url = "https://files.pythonhosted.org/packages/02/71/0aa64aeda645510af0a43f7d9ee70932f0d1dc4263aed34c50ee891d9df3/sqlalchemy-2.0.51-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0378d055e9e8cd6ce4d8dff683bdd3d7d413533c4ee51d67a2b1e0f9eacc0f23", size = 3475355, upload-time = "2026-06-15T15:58:36.592Z" }, + { url = "https://files.pythonhosted.org/packages/05/db/6061db32316446135a3abae5f308d144ab988a34234726042da3e58b1c63/sqlalchemy-2.0.51-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6e46fc36029eff666391e0531e5387b62ce6c4f1d8e50b3fb3099eaca1b42522", size = 3486591, upload-time = "2026-06-15T16:02:45.346Z" }, + { url = "https://files.pythonhosted.org/packages/0d/c9/f14fdf71bb8957e0c7e39db69bbdf12b5c80f4ef775fdfa127bf4e0d6760/sqlalchemy-2.0.51-cp314-cp314t-win32.whl", hash = "sha256:9161cfc9efce70d1715f47d6ff40f79c6778c00d53be4fbc09d70301e4b83ba7", size = 2151313, upload-time = "2026-06-15T16:03:39.127Z" }, + { url = "https://files.pythonhosted.org/packages/6a/c6/673e618e6f4f297e126d9b56ea2f6478708f6c1af4e3223835c22e2c3697/sqlalchemy-2.0.51-cp314-cp314t-win_amd64.whl", hash = "sha256:159bb6ba32059f57ad7375a8f50d844dd2f19d14954ecf820cd33e20debd46b2", size = 2186280, upload-time = "2026-06-15T16:03:40.569Z" }, + { url = "https://files.pythonhosted.org/packages/e2/22/dbf013a12ec759e54a34a119e9e217435b3f71b2dd5c61a7ade0a25dae87/sqlalchemy-2.0.51-py3-none-any.whl", hash = "sha256:bb024d8b621d0be75f4f44ecc7c950450026e76d66dc8f791bb5331d7fed59d5", size = 1944334, upload-time = "2026-06-15T16:09:22.418Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size = 113555, upload-time = "2026-07-02T08:40:05.92Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, +]