From f0ad4b356cfa4b72f22926c91800cc45fcc9f3c7 Mon Sep 17 00:00:00 2001 From: vmihalovski Date: Thu, 17 Sep 2026 17:25:01 +0200 Subject: [PATCH 1/4] Feat(ontology): add Ossie -> RelationalAI converter with formula parsing and reasoner --- converters/ontology/.gitignore | 2 +- converters/ontology/README.md | 67 +- converters/ontology/pyproject.toml | 14 +- .../ontology/scripts/palantir_to_ossie.py | 2 +- .../ontology/src/ossie_ontology/__init__.py | 10 +- .../ossie_to_relationalai/__init__.py | 47 + .../ossie_to_relationalai/converter.py | 569 ++++ .../converter/palantir_to_ossie/converter.py | 7 +- .../converter/spec_to_ossie/converter.py | 39 +- .../src/ossie_ontology/expr/__init__.py | 45 + .../src/ossie_ontology/expr/common.py | 39 + .../src/ossie_ontology/expr/factory.py | 193 ++ .../ossie_ontology/expr/formula/__init__.py | 17 + .../src/ossie_ontology/expr/formula/lexer.py | 126 + .../src/ossie_ontology/expr/formula/model.py | 410 +++ .../src/ossie_ontology/expr/formula/parser.py | 335 ++ .../ossie_ontology/expr/formula/validator.py | 330 ++ .../expr/formula/visitor/__init__.py | 231 ++ .../expr/formula/visitor/collector.py | 139 + .../expr/formula/visitor/converter.py | 394 +++ .../expr/formula/visitor/unbound.py | 220 ++ .../ontology/src/ossie_ontology/expr/model.py | 77 + .../ontology/src/ossie_ontology/model.py | 14 +- .../src/ossie_ontology/parser/__init__.py | 8 +- .../ontology/src/ossie_ontology/reasoner.py | 159 + .../src/ossie_ontology/vendor/__init__.py | 26 + .../{external => vendor/palantir}/__init__.py | 0 .../{external => vendor}/palantir/model.py | 0 .../palantir/parser/__init__.py | 2 +- .../relationalai}/__init__.py | 0 .../vendor/relationalai/mappings.py | 385 +++ .../vendor/relationalai/ontology.py | 309 ++ .../vendor/relationalai/roles.py | 100 + converters/ontology/tests/common/__init__.py | 18 + .../ontology/tests/{ => common}/test_graph.py | 0 .../test_text_utils.py} | 0 .../ontology/tests/config/raiconfig.yaml | 50 + converters/ontology/tests/conftest.py | 100 +- converters/ontology/tests/expr/__init__.py | 18 + .../ontology/tests/expr/test_parsing.py | 237 ++ .../ontology/tests/expr/test_validator.py | 71 + .../behaviours/derived_identifier.yaml | 53 + .../behaviours/key_column_both_ways.yaml | 179 ++ .../tests/fixtures/behaviours/ref_scheme.yaml | 167 + .../palantir/retail_mini/data_sets/ds.json | 34 + .../palantir/retail_mini/ontology.json | 75 + .../tests/fixtures/{ => specs}/flights.yaml | 0 .../ontology/tests/fixtures/specs/retail.yaml | 1116 +++++++ .../ontology/tests/fixtures/specs/tpch.yaml | 1211 ++++++++ .../validation/ambiguous_type_vars.yaml | 41 + .../validation/bare_field_in_formula.yaml | 39 + .../mapping_formula_agg_with_rel.yaml | 80 + .../mapping_formula_concept_ref.yaml | 65 + .../mapping_formula_unknown_field.yaml | 65 + .../validation/mapping_formula_valid.yaml | 67 + .../not_declared_concept_in_identifier.yaml | 23 + .../not_declared_concept_in_relationship.yaml | 42 + .../require_sub_exists_unknown_rel.yaml | 54 + .../validation/require_sub_exists_valid.yaml | 56 + .../scoped_require_scope_valid.yaml | 158 + .../validation/signature_mismatch.yaml | 41 + .../top_level_require_exists_valid.yaml | 33 + .../top_level_require_free_var.yaml | 33 + .../top_level_require_unknown_concept.yaml | 33 + .../validation/top_level_require_valid.yaml | 33 + .../validation/undefined_relationship.yaml | 39 + .../validation/unresolved_var_type.yaml | 39 + converters/ontology/tests/model/__init__.py | 18 + .../tests/{ => model}/test_concept_lookup.py | 0 .../ontology/tests/{ => model}/test_roles.py | 0 .../test_verbalization.py} | 0 .../ontology/tests/palantir/__init__.py | 24 + .../tests/palantir/converter/__init__.py | 38 + .../tests/palantir/converter/builders.py | 183 ++ .../tests/palantir/converter/helpers.py | 122 + .../palantir/converter/test_inheritance.py | 245 ++ .../palantir/converter/test_many_to_many.py | 133 + .../converter/test_multi_dataset_links.py | 126 + .../converter/test_object_type_statuses.py | 78 + .../palantir/converter/test_primary_keys.py | 108 + .../converter/test_property_statuses.py | 83 + .../palantir/converter/test_property_types.py | 269 ++ .../converter/test_relation_statuses.py | 145 + .../test_parser.py} | 4 +- .../ontology/tests/relationalai/__init__.py | 24 + .../flights/flights.py | 199 ++ .../retail/retail.py | 179 ++ .../tpch/tpch.py | 192 ++ .../palantir_chain.py | 41 + .../tests/relationalai/test_converter.py | 300 ++ converters/ontology/tests/spec/__init__.py | 18 + .../flights/roundtrip.yaml} | 0 .../retail/roundtrip.yaml | 1192 +++++++ .../tpch/roundtrip.yaml | 1282 ++++++++ .../flights/structure.txt} | 0 .../retail/structure.txt | 137 + .../tpch/structure.txt | 118 + .../test_corpus.py} | 35 +- .../test_parser.py} | 89 + .../ontology/tests/test_examples_in_sync.py | 4 +- converters/ontology/tests/test_offline.py | 81 + .../ontology/tests/test_optional_extra.py | 120 + .../ontology/tests/test_palantir_converter.py | 1212 -------- converters/ontology/tests/test_reasoner.py | 188 ++ converters/ontology/uv.lock | 2740 ++++++++++++++++- 105 files changed, 16920 insertions(+), 1393 deletions(-) create mode 100644 converters/ontology/src/ossie_ontology/converter/ossie_to_relationalai/__init__.py create mode 100644 converters/ontology/src/ossie_ontology/converter/ossie_to_relationalai/converter.py create mode 100644 converters/ontology/src/ossie_ontology/expr/__init__.py create mode 100644 converters/ontology/src/ossie_ontology/expr/common.py create mode 100644 converters/ontology/src/ossie_ontology/expr/factory.py create mode 100644 converters/ontology/src/ossie_ontology/expr/formula/__init__.py create mode 100644 converters/ontology/src/ossie_ontology/expr/formula/lexer.py create mode 100644 converters/ontology/src/ossie_ontology/expr/formula/model.py create mode 100644 converters/ontology/src/ossie_ontology/expr/formula/parser.py create mode 100644 converters/ontology/src/ossie_ontology/expr/formula/validator.py create mode 100644 converters/ontology/src/ossie_ontology/expr/formula/visitor/__init__.py create mode 100644 converters/ontology/src/ossie_ontology/expr/formula/visitor/collector.py create mode 100644 converters/ontology/src/ossie_ontology/expr/formula/visitor/converter.py create mode 100644 converters/ontology/src/ossie_ontology/expr/formula/visitor/unbound.py create mode 100644 converters/ontology/src/ossie_ontology/expr/model.py create mode 100644 converters/ontology/src/ossie_ontology/reasoner.py create mode 100644 converters/ontology/src/ossie_ontology/vendor/__init__.py rename converters/ontology/src/ossie_ontology/{external => vendor/palantir}/__init__.py (100%) rename converters/ontology/src/ossie_ontology/{external => vendor}/palantir/model.py (100%) rename converters/ontology/src/ossie_ontology/{external => vendor}/palantir/parser/__init__.py (99%) rename converters/ontology/src/ossie_ontology/{external/palantir => vendor/relationalai}/__init__.py (100%) create mode 100644 converters/ontology/src/ossie_ontology/vendor/relationalai/mappings.py create mode 100644 converters/ontology/src/ossie_ontology/vendor/relationalai/ontology.py create mode 100644 converters/ontology/src/ossie_ontology/vendor/relationalai/roles.py create mode 100644 converters/ontology/tests/common/__init__.py rename converters/ontology/tests/{ => common}/test_graph.py (100%) rename converters/ontology/tests/{test_verbalization_strings.py => common/test_text_utils.py} (100%) create mode 100644 converters/ontology/tests/config/raiconfig.yaml create mode 100644 converters/ontology/tests/expr/__init__.py create mode 100644 converters/ontology/tests/expr/test_parsing.py create mode 100644 converters/ontology/tests/expr/test_validator.py create mode 100644 converters/ontology/tests/fixtures/behaviours/derived_identifier.yaml create mode 100644 converters/ontology/tests/fixtures/behaviours/key_column_both_ways.yaml create mode 100644 converters/ontology/tests/fixtures/behaviours/ref_scheme.yaml create mode 100644 converters/ontology/tests/fixtures/palantir/retail_mini/data_sets/ds.json create mode 100644 converters/ontology/tests/fixtures/palantir/retail_mini/ontology.json rename converters/ontology/tests/fixtures/{ => specs}/flights.yaml (100%) create mode 100644 converters/ontology/tests/fixtures/specs/retail.yaml create mode 100644 converters/ontology/tests/fixtures/specs/tpch.yaml create mode 100644 converters/ontology/tests/fixtures/validation/ambiguous_type_vars.yaml create mode 100644 converters/ontology/tests/fixtures/validation/bare_field_in_formula.yaml create mode 100644 converters/ontology/tests/fixtures/validation/mapping_formula_agg_with_rel.yaml create mode 100644 converters/ontology/tests/fixtures/validation/mapping_formula_concept_ref.yaml create mode 100644 converters/ontology/tests/fixtures/validation/mapping_formula_unknown_field.yaml create mode 100644 converters/ontology/tests/fixtures/validation/mapping_formula_valid.yaml create mode 100644 converters/ontology/tests/fixtures/validation/not_declared_concept_in_identifier.yaml create mode 100644 converters/ontology/tests/fixtures/validation/not_declared_concept_in_relationship.yaml create mode 100644 converters/ontology/tests/fixtures/validation/require_sub_exists_unknown_rel.yaml create mode 100644 converters/ontology/tests/fixtures/validation/require_sub_exists_valid.yaml create mode 100644 converters/ontology/tests/fixtures/validation/scoped_require_scope_valid.yaml create mode 100644 converters/ontology/tests/fixtures/validation/signature_mismatch.yaml create mode 100644 converters/ontology/tests/fixtures/validation/top_level_require_exists_valid.yaml create mode 100644 converters/ontology/tests/fixtures/validation/top_level_require_free_var.yaml create mode 100644 converters/ontology/tests/fixtures/validation/top_level_require_unknown_concept.yaml create mode 100644 converters/ontology/tests/fixtures/validation/top_level_require_valid.yaml create mode 100644 converters/ontology/tests/fixtures/validation/undefined_relationship.yaml create mode 100644 converters/ontology/tests/fixtures/validation/unresolved_var_type.yaml create mode 100644 converters/ontology/tests/model/__init__.py rename converters/ontology/tests/{ => model}/test_concept_lookup.py (100%) rename converters/ontology/tests/{ => model}/test_roles.py (100%) rename converters/ontology/tests/{test_verbalization_parsing.py => model/test_verbalization.py} (100%) create mode 100644 converters/ontology/tests/palantir/__init__.py create mode 100644 converters/ontology/tests/palantir/converter/__init__.py create mode 100644 converters/ontology/tests/palantir/converter/builders.py create mode 100644 converters/ontology/tests/palantir/converter/helpers.py create mode 100644 converters/ontology/tests/palantir/converter/test_inheritance.py create mode 100644 converters/ontology/tests/palantir/converter/test_many_to_many.py create mode 100644 converters/ontology/tests/palantir/converter/test_multi_dataset_links.py create mode 100644 converters/ontology/tests/palantir/converter/test_object_type_statuses.py create mode 100644 converters/ontology/tests/palantir/converter/test_primary_keys.py create mode 100644 converters/ontology/tests/palantir/converter/test_property_statuses.py create mode 100644 converters/ontology/tests/palantir/converter/test_property_types.py create mode 100644 converters/ontology/tests/palantir/converter/test_relation_statuses.py rename converters/ontology/tests/{test_palantir_parser.py => palantir/test_parser.py} (98%) create mode 100644 converters/ontology/tests/relationalai/__init__.py create mode 100644 converters/ontology/tests/relationalai/snapshots/test_converter/test_converted_model_decompiles_to_expected_pyrel/flights/flights.py create mode 100644 converters/ontology/tests/relationalai/snapshots/test_converter/test_converted_model_decompiles_to_expected_pyrel/retail/retail.py create mode 100644 converters/ontology/tests/relationalai/snapshots/test_converter/test_converted_model_decompiles_to_expected_pyrel/tpch/tpch.py create mode 100644 converters/ontology/tests/relationalai/snapshots/test_converter/test_palantir_export_converts_all_the_way_to_pyrel/palantir_chain.py create mode 100644 converters/ontology/tests/relationalai/test_converter.py create mode 100644 converters/ontology/tests/spec/__init__.py rename converters/ontology/tests/{snapshots/test_flights_snapshot/test_flights_roundtrip_yaml_snapshot/flights_roundtrip.yaml => spec/snapshots/test_corpus/test_roundtrip_yaml_snapshot/flights/roundtrip.yaml} (100%) create mode 100644 converters/ontology/tests/spec/snapshots/test_corpus/test_roundtrip_yaml_snapshot/retail/roundtrip.yaml create mode 100644 converters/ontology/tests/spec/snapshots/test_corpus/test_roundtrip_yaml_snapshot/tpch/roundtrip.yaml rename converters/ontology/tests/{snapshots/test_flights_snapshot/test_flights_structure_snapshot/flights_structure.txt => spec/snapshots/test_corpus/test_structure_snapshot/flights/structure.txt} (100%) create mode 100644 converters/ontology/tests/spec/snapshots/test_corpus/test_structure_snapshot/retail/structure.txt create mode 100644 converters/ontology/tests/spec/snapshots/test_corpus/test_structure_snapshot/tpch/structure.txt rename converters/ontology/tests/{test_flights_snapshot.py => spec/test_corpus.py} (64%) rename converters/ontology/tests/{test_ossie_parser.py => spec/test_parser.py} (73%) create mode 100644 converters/ontology/tests/test_offline.py create mode 100644 converters/ontology/tests/test_optional_extra.py delete mode 100644 converters/ontology/tests/test_palantir_converter.py create mode 100644 converters/ontology/tests/test_reasoner.py diff --git a/converters/ontology/.gitignore b/converters/ontology/.gitignore index 553c47ac..eaeeea30 100644 --- a/converters/ontology/.gitignore +++ b/converters/ontology/.gitignore @@ -56,4 +56,4 @@ htmlcov/ .idea/ *.iml *.iws -*.ipr \ No newline at end of file +*.ipr diff --git a/converters/ontology/README.md b/converters/ontology/README.md index f47a1277..01a68e3f 100644 --- a/converters/ontology/README.md +++ b/converters/ontology/README.md @@ -19,13 +19,68 @@ # Ossie Ontology Converters -Converters between Ossie, Palantir, and Spec ontology formats. +Converters between Ossie, Palantir, Spec, and RelationalAI ontology formats. -| Converter | Direction | -|---------------------|-----------| -| `palantir_to_ossie` | Palantir ontology → Ossie model | -| `ossie_to_spec` | Ossie model → Spec YAML | -| `spec_to_ossie` | Spec YAML → Ossie model | +| Converter | Direction | +|-------------------------|-----------| +| `palantir_to_ossie` | Palantir ontology → Ossie model | +| `ossie_to_spec` | Ossie model → Spec YAML | +| `spec_to_ossie` | Spec YAML → Ossie model | +| `ossie_to_relationalai` | Ossie model → RelationalAI (PyRel) | + +### The `relationalai` extra + +`ossie_to_relationalai` is the only converter that needs a vendor SDK, so it is +gated behind an optional extra rather than the base install: + +```bash +pip install "apache-ossie-ontology[relationalai]" +``` + +Nothing reachable from `ossie_ontology/__init__.py` imports `relationalai`, so +everything else — parsing Ossie, converting Palantir, reading and writing the +spec — installs and runs without it. Its tests skip themselves when it is absent. + +The PyRel-side model it targets — `OntologyModel` and its bindings, roles and +CSV plumbing — lives under `ossie_ontology/vendor/relationalai/`, +next to `ossie_ontology/vendor/palantir/`. The converter package itself holds +only the translation. The one piece that sits elsewhere is the formula emitter, +`ossie_ontology/expr/formula/visitor/converter.py`, which stays with the other +formula visitors it is a variant of. + +It converts an `OssieOntology` into an in-memory `OntologyModel`, which can then +be serialized to PyRel source: + +```python +from pathlib import Path + +from relationalai.semantics.metamodel.pyrel_codegen import to_pyrel + +from ossie_ontology.parser import OssieParser +from ossie_ontology.converter.ossie_to_relationalai import OssieToRelationalAIConverter + +ontology = OssieParser().parse(Path("model.yaml")) + +model = OssieToRelationalAIConverter.convert(ontology) +Path("model_pyrel.py").write_text(to_pyrel(model.base_model().to_metamodel())) +``` + +`OssieParser` parses and validates `derived_by` and `requires` expressions into +an AST by default, which is what the conversion needs — an unparsed formula is +skipped and never reaches PyRel. To keep formulas as raw text instead, pass the +plain `FormulaFactory` and `MappingFormulaFactory` from `ossie_ontology.model`. +`SpecToOssieConverter` and `PalantirToOssieConverter` take the same argument and +default the same way. + +Two things about the environment this needs. Constructing a model makes +`relationalai` resolve its configuration, so a `raiconfig.yaml` must be present. +And each dataset's `source` is read from the configured connection to get its +column types, so the call above needs one that can reach those tables. + +To convert without a warehouse, pass `use_csv_only=True`: every dataset is then +treated as an inline CSV rather than looked up, and nothing leaves the process. +That is how the test suite runs — see `tests/conftest.py` for the offline config +it pins, and `tests/test_ossie_to_relationalai.py` for the full offline setup. ## Prerequisites diff --git a/converters/ontology/pyproject.toml b/converters/ontology/pyproject.toml index af6c7ea8..6ff2e786 100644 --- a/converters/ontology/pyproject.toml +++ b/converters/ontology/pyproject.toml @@ -40,9 +40,20 @@ keywords = [ ] dependencies = [ "pydantic", + "ply", "pyyaml", ] +# Extras are opt-in: `pip install "apache-ossie-ontology[relationalai]"`. +# `relationalai` is the vendor SDK the Ossie -> RelationalAI converter emits +# against. Parsing Ossie, converting Palantir, and reading/writing the spec all +# work without it, and nothing reachable from `ossie_ontology/__init__.py` +# imports it — so it stays out of the base install. +[project.optional-dependencies] +relationalai = [ + "relationalai==1.27.1", +] + [project.urls] homepage = "https://ossie.apache.org/" repository = "https://github.com/apache/ossie/" @@ -55,9 +66,10 @@ testpaths = ["tests"] [tool.pyright] pythonVersion = "3.11" +exclude = ["tests/snapshots", "build", "venv"] [tool.uv] required-version = ">=0.9.0" default-groups = [ "dev" -] \ No newline at end of file +] diff --git a/converters/ontology/scripts/palantir_to_ossie.py b/converters/ontology/scripts/palantir_to_ossie.py index e1166c66..9f77a52e 100644 --- a/converters/ontology/scripts/palantir_to_ossie.py +++ b/converters/ontology/scripts/palantir_to_ossie.py @@ -49,7 +49,7 @@ from ossie_ontology.converter.palantir_to_ossie.converter import PalantirToOssieConverter from ossie_ontology.converter.ossie_to_spec.converter import OssieToSpecConverter -from ossie_ontology.external.palantir.parser import PalantirParser +from ossie_ontology.vendor.palantir.parser import PalantirParser if __name__ == "__main__": db_name = os.environ.get("SNOWFLAKE_DATABASE_NAME", "PALANTIR") diff --git a/converters/ontology/src/ossie_ontology/__init__.py b/converters/ontology/src/ossie_ontology/__init__.py index 9579ef3b..1f5fab30 100644 --- a/converters/ontology/src/ossie_ontology/__init__.py +++ b/converters/ontology/src/ossie_ontology/__init__.py @@ -46,8 +46,10 @@ SemanticModel, ) from ossie_ontology.spec import OssieSpec +from ossie_ontology.reasoner import OntologyReasoner from ossie_ontology.parser import OssieParser -from ossie_ontology.external.palantir.parser import PalantirParser +from ossie_ontology.expr.factory import FormulaParserFactory, MappingFormulaParserFactory +from ossie_ontology.vendor.palantir.parser import PalantirParser from ossie_ontology.converter.spec_to_ossie.converter import SpecToOssieConverter from ossie_ontology.converter.ossie_to_spec.converter import OssieToSpecConverter from ossie_ontology.converter.palantir_to_ossie.converter import PalantirToOssieConverter @@ -81,9 +83,15 @@ "FormulaFactory", # Spec DTO "OssieSpec", + # Reasoner + "OntologyReasoner", # Parsers "OssieParser", "PalantirParser", + # Formula parsing — plug these into OssieParser/PalantirParser to get + # formulas parsed into an AST rather than kept as raw strings. + "FormulaParserFactory", + "MappingFormulaParserFactory", # Converters "SpecToOssieConverter", "OssieToSpecConverter", diff --git a/converters/ontology/src/ossie_ontology/converter/ossie_to_relationalai/__init__.py b/converters/ontology/src/ossie_ontology/converter/ossie_to_relationalai/__init__.py new file mode 100644 index 00000000..c3f0637f --- /dev/null +++ b/converters/ontology/src/ossie_ontology/converter/ossie_to_relationalai/__init__.py @@ -0,0 +1,47 @@ +# 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. + +"""Ossie -> RelationalAI (PyRel) conversion. + +Importing anything from this package imports `relationalai`, which ships as the +optional `relationalai` extra. Nothing under `ossie_ontology` imports this +package at module scope, so the rest of the library stays installable — and +importable — without the SDK present. + +The model this converter targets is not here: `OntologyModel` and the rest of +the PyRel-side types live under `ossie_ontology.vendor.relationalai`, the same +way the Palantir model lives under `ossie_ontology.vendor.palantir`. What is +left here is the translation itself. +""" + +from ossie_ontology.converter.ossie_to_relationalai.converter import ( + OssieToRelationalAIConverter, + TableProvider, + declared_table, + warehouse_table, +) +from ossie_ontology.vendor.relationalai.ontology import OntologyModel + +__all__ = [ + "OssieToRelationalAIConverter", + "OntologyModel", + # The dataset extension point: swap `table_provider` to read datasets from + # somewhere other than the warehouse their `source` names. + "TableProvider", + "warehouse_table", + "declared_table", +] diff --git a/converters/ontology/src/ossie_ontology/converter/ossie_to_relationalai/converter.py b/converters/ontology/src/ossie_ontology/converter/ossie_to_relationalai/converter.py new file mode 100644 index 00000000..4bcf7ee9 --- /dev/null +++ b/converters/ontology/src/ossie_ontology/converter/ossie_to_relationalai/converter.py @@ -0,0 +1,569 @@ +# 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. + +"""Ossie -> RelationalAI: an `OssieOntology` becomes a PyRel `OntologyModel`. + +Together with `ossie_ontology.expr.formula.visitor.converter` — the formula +emitter, which stays with its sibling visitors — this package is the only part +of `ossie_ontology` that imports `relationalai`. Neither is reachable from +`ossie_ontology/__init__.py`, which is what lets the SDK be an optional extra +rather than a hard dependency. Install it with: + + pip install "apache-ossie-ontology[relationalai]" +""" +from __future__ import annotations + +import warnings +from typing import Any, Callable + +from relationalai.util.naming import NameCache +from ossie_ontology.vendor.relationalai.mappings import EntityMapping +from ossie_ontology.vendor.relationalai.ontology import OntologyModel +from ossie_ontology.expr.formula.model import Formula +from ossie_ontology.expr.formula.visitor.converter import FormulaConverter, MappingFormulaConverter +from ossie_ontology.reasoner import OntologyReasoner +from ossie_ontology.model import ( + Concept, + Dataset, + DatasetField, + LinkMapping, + ObjectMapping, + SemanticModel, + OntologyComponent, + OntologyMapping, + ReferentMapping, + Relationship, + OssieOntology, +) +import relationalai.semantics as pyrel +from relationalai.semantics import Concept as RAIConcept + +# How a dataset becomes something the model can read. Takes the model being +# built, the dataset as the Ossie spec declares it, and the column types derived +# from that spec; returns whatever pyrel construct should stand in for it. +# +# The default resolves the dataset's `source` to a warehouse table. Substituting +# a provider is how a caller reads from somewhere else — a downstream project +# supplies one backed by inline CSV so its tests need no warehouse — without +# this converter having to know that any such alternative exists. +TableProvider = Callable[["OntologyModel", Dataset, dict[str, RAIConcept]], Any] + + +def warehouse_table(ontology: "OntologyModel", dataset: Dataset, + schema: dict[str, RAIConcept]) -> Any: + """Resolve a dataset to the warehouse table its `source` names. + + The schema is deliberately *not* passed on: pyrel reads the real columns + from the table instead. That costs a connection when the model is compiled, + but it is the only way to get the column identifiers right — Snowflake folds + an unquoted name to upper case, so declaring the spec's `airport_code` + yields `AIRPORT_CODE`, which does not address a column created as + `"airport_code"`. Only the table itself knows which it is. + + Use :func:`declared_table` where the spec's field names are known to match + and a connection is unwanted. + """ + return ontology.base_model().Table(dataset.source) + + +def declared_table(ontology: "OntologyModel", dataset: Dataset, + schema: dict[str, RAIConcept]) -> Any: + """Resolve a dataset to its table, declaring the columns from the spec. + + Nothing is read from the warehouse, so conversion and compilation work with + no connection — which is what makes an offline test suite possible. The + tradeoff is the one :func:`warehouse_table` avoids: the declared identifiers + have to match how the table was actually created. + """ + return ontology.base_model().Table(dataset.source, schema=schema) + + +class OssieToRelationalAIConverter: + + @staticmethod + def convert( + model: OssieOntology, + reasoner: OntologyReasoner | None = None, + model_cls: type[OntologyModel] = OntologyModel, + table_provider: TableProvider = warehouse_table, + ) -> OntologyModel: + """Convert an OssieOntology into a PyRel model. + + `model_cls` decides what the result is an instance of. The conversion + itself only ever touches the plain `OntologyModel` surface, so the + default is that class; pass `MeasureOntologyModel` when the caller goes + on to ask dimensional measure queries of the result. + """ + ontology = model_cls(model.name) + properties_index: dict[str, pyrel.Relationship] = {} + + oc = model.ontology + if reasoner is None: + reasoner = OntologyReasoner() + oc.register(reasoner) + + OssieToRelationalAIConverter._convert_concepts(oc, ontology) + OssieToRelationalAIConverter._convert_relationships(oc, ontology, properties_index, reasoner) + OssieToRelationalAIConverter._identify_concepts(oc, ontology, properties_index) + + for om in model.ontology_mappings: + OssieToRelationalAIConverter._convert_datasets( + om.semantic_model, ontology, table_provider + ) + + # Shared across every ontology_mapping, so a binding declared in two of + # them collapses to one creation rule. Insurance rather than a live + # saving: every scenario in the corpus has a single ontology_mapping. + entity_bindings: dict = {} + for om in model.ontology_mappings: + OssieToRelationalAIConverter._convert_mappings( + om, ontology, properties_index, reasoner, entity_bindings + ) + + OssieToRelationalAIConverter._convert_formulas(oc, ontology, reasoner) + + return ontology + + @staticmethod + def _concept(ontology: OntologyModel, name: str) -> RAIConcept: + """The pyrel concept for an Ossie concept name. + + Every concept is created by `_convert_concepts` before any mapping or + reading is converted, so a miss means those passes disagree about the + ontology. Failing here names the concept; letting the None through + surfaces later as an opaque `NoneType has no attribute` from inside + pyrel, or binds a null silently. + """ + concept = ontology.lookup_concept(name) + assert concept is not None, f"Concept '{name}' not found in the converted ontology" + return concept + + # ------------------------------------------------------------------ + # Datasets + # ------------------------------------------------------------------ + + @staticmethod + def _convert_datasets(sm: SemanticModel, ontology: OntologyModel, table_provider: TableProvider) -> None: + for dataset in sm.datasets: + schema = OssieToRelationalAIConverter._pyrel_schema(dataset, ontology) + ontology.add_table(dataset.name, table_provider(ontology, dataset, schema)) + + @staticmethod + def _pyrel_schema(dataset: Dataset, ontology: OntologyModel) -> dict: + """Column types for *dataset*, taken from the Ossie spec that declares it. + + Handed to the table provider so it can declare the columns up front, + which is what keeps the conversion from needing a live warehouse. + """ + schema = {} + for fl in dataset.fields: + if fl.type is not None: + concept = OssieToRelationalAIConverter._concept(ontology, fl.type.name) + schema[fl.name] = ontology.get_topmost_parent(concept) + else: + schema[fl.name] = ontology.lookup_concept("String") + return schema + + # ------------------------------------------------------------------ + # Concepts + # ------------------------------------------------------------------ + + @staticmethod + def _convert_concepts(oc: OntologyComponent, ontology: OntologyModel) -> None: + sorted_names = OssieToRelationalAIConverter._sort_dependency_graph(oc.concepts(exclude_builtin=True)) + for name in sorted_names: + concept = oc.lookup_concept(name) + if concept and concept.name not in ontology.base_model().concept_index: + subtypes = [] + for parent in concept.extends: + pyrel_parent = ontology.lookup_concept(parent.name) + if pyrel_parent is None: + raise ValueError(f"'{parent.name}' is not declared in the ontology.") + subtypes.append(pyrel_parent) + ontology.Concept(concept.name, extends=subtypes) + + # ------------------------------------------------------------------ + # Relationships + # ------------------------------------------------------------------ + + @staticmethod + def _convert_relationships(oc: OntologyComponent, ontology: OntologyModel, properties_index: dict, reasoner: OntologyReasoner) -> None: + for rel in oc.relationships: + container = rel.first_role.player + if rel.name in pyrel.Concept.RESERVED_NAMES: + warnings.warn( + f"Relationship '{rel.full_name}' clashes with Concept reserved name '{rel.name}'", + UserWarning, + stacklevel=2, + ) + continue + reading = OssieToRelationalAIConverter._generate_reading(rel, ontology) + if reasoner.is_property(rel): + pyrel_rel = ontology.Property(reading, short_name=rel.name) + else: + pyrel_rel = ontology.Relationship(reading, short_name=rel.name) + properties_index[rel.full_name] = pyrel_rel + if not container.is_builtin: + c = ontology.lookup_concept(container.name) + c.__setattr__(rel.name, pyrel_rel) + + # ------------------------------------------------------------------ + # Identifiers + # ------------------------------------------------------------------ + + @staticmethod + def _identify_concepts(oc: OntologyComponent, ontology: OntologyModel, properties_index: dict) -> None: + for concept in oc.concepts(exclude_builtin=True): + identify_by = concept.identify_by + if identify_by: + pyrel_concept = OssieToRelationalAIConverter._concept(ontology, concept.name) + props = [properties_index[key] for key in identify_by] + pyrel_concept.identify_by(*props) + + # ------------------------------------------------------------------ + # Mappings + # ------------------------------------------------------------------ + + @staticmethod + def _expression_key(expression) -> tuple: + """Dataset-qualified identity of a mapping expression. + + A ``DatasetField`` stringifies to the bare column name (``runway_id``), + which is *not* unique: att_latest maps ``OFT_Runway`` twice from two + different datasets that both have a ``runway_id`` column. Keying on the + rendered string alone merges those two bindings and rebinds the second + concept mapping's properties onto the first one's dataset. The owning + dataset therefore has to be part of the key — by identity, since two + distinct datasets may also share a name. + """ + if isinstance(expression, DatasetField): + dataset = expression.dataset + return ("field", getattr(dataset, "name", None), id(dataset), expression.name) + # Formulas render their dataset references inline, so the text is + # already qualified. + return ("other", str(expression)) + + @staticmethod + def _object_mapping_key(osi_concept: Concept, om: ObjectMapping) -> tuple: + """Identity of the entity binding an ObjectMapping would produce. + + Two object mappings that agree on concept *and* on how the identifier + is sourced describe the same set of entities, so they only need one + creation rule between them. Everything the identifier is derived from + goes into the key: the referent mappings (relationship + expression), + or — for the bare-expression form — the expression and the concept it + casts to. `_build_entity_binding` also reads ``om.concept`` (the + supertype that supplies the ref scheme), so that is part of the key too. + """ + supertype = om.concept.name if om.concept is not None else None + if om.referent_mappings: + identifier = ( + "referents", + tuple(sorted( + (str(getattr(rm.relationship, "name", rm.relationship)), + OssieToRelationalAIConverter._expression_key(rm.expression)) + for rm in om.referent_mappings + )), + ) + elif om.expression is not None: + identifier = ("expression", OssieToRelationalAIConverter._expression_key(om.expression)) + else: + identifier = ("none",) + return (osi_concept.name, supertype, identifier) + + @staticmethod + def _convert_mappings(om: OntologyMapping, ontology: OntologyModel, properties_index: dict, + reasoner: OntologyReasoner, entity_bindings: dict | None = None) -> None: + # A concept is routinely mapped twice: once standalone under + # `object_mappings` and again as the `object_mapping` of a + # `link_mappings` root that hangs properties off it. Both used to build + # their own EBinding, and every EBinding emits `define(C.new(...))` + # (see `mappings.EBinding.__init__`), so the model ended up with two + # identical creation rules — idempotent, but duplicated work in every + # compiled model. Bindings are now cached by + # `_object_mapping_key`, so the second reference reuses the first + # binding instead of re-declaring it. `Eref` is immutable once built and + # a binding already drives many `binds_to` calls, so sharing is safe. + # + # Note this cannot be simplified to "never create from link roots": + # some concepts (Flight in the flights scenarios, CardPayment / + # CashPayment / Item in retail_refactored) have no `object_mappings` + # entry at all and are created solely by their link root. + if entity_bindings is None: + entity_bindings = {} + for cm in om.concept_mappings: + entity_concept = ontology.lookup_concept(cm.concept.name) + for obj_m in cm.object_mappings: + OssieToRelationalAIConverter._entity_binding_for( + obj_m, cm.concept, entity_concept, ontology, reasoner, entity_bindings + ) + for root_lm in cm.link_mappings: + OssieToRelationalAIConverter._convert_root_link_mapping( + root_lm, cm.concept, entity_concept, ontology, properties_index, reasoner, + entity_bindings + ) + + @staticmethod + def _entity_binding_for(om: ObjectMapping, osi_concept: Concept, entity_concept, + ontology: OntologyModel, reasoner: OntologyReasoner, + entity_bindings: dict): + """Return the binding for *om*, building it only on first sight.""" + key = OssieToRelationalAIConverter._object_mapping_key(osi_concept, om) + binding = entity_bindings.get(key) + if binding is None: + binding = OssieToRelationalAIConverter._build_entity_binding( + om, osi_concept, entity_concept, ontology, reasoner + ) + entity_bindings[key] = binding + return binding + + @staticmethod + def _build_entity_binding(om: ObjectMapping, osi_concept: Concept, entity_concept: RAIConcept, + ontology: OntologyModel, reasoner: OntologyReasoner): + if om.referent_mappings: + kwargs = OssieToRelationalAIConverter._build_entity_kwargs(om.referent_mappings, ontology) + elif om.expression is not None: + scheme = reasoner.ref_scheme(osi_concept) + if not scheme: + raise ValueError( + f"Concept '{osi_concept.name}' has no reference scheme; " + "cannot map a bare expression without referent_mappings." + ) + if len(scheme) != 1: + raise ValueError( + f"Concept '{osi_concept.name}' has a composite reference scheme; " + "use referent_mappings instead of a bare expression." + ) + id_rel = scheme[0] + kwargs = {id_rel.name: OssieToRelationalAIConverter._render_expression(om.expression, ontology)} + else: + kwargs = {} + if om.concept is not None: + parent_concept = OssieToRelationalAIConverter._concept(ontology, om.concept.name) + return ontology.EntityBinding(entity_concept, stype=parent_concept, **kwargs) + return ontology.EntityBinding(entity_concept, **kwargs) + + @staticmethod + def _render_expression(expression, ontology: OntologyModel, concept=None): + if isinstance(expression, DatasetField): + # `dataset` is a back-pointer set when the field is attached to its + # dataset; a field that never was cannot be resolved to a table. + dataset = expression.dataset + assert dataset is not None, ( + f"Mapping expression references column '{expression.name}', which is " + f"not attached to any dataset" + ) + table = ontology.lookup_table(dataset.name) + assert table is not None, ( + f"Dataset '{dataset.name}' has no table in the converted ontology; " + f"it is referenced by the mapping for column '{expression.name}'" + ) + result = table.__getattr__(expression.name) + return concept(result) if concept is not None else result + if isinstance(expression, Formula): + return MappingFormulaConverter(ontology, expression, concept=concept).result + raise ValueError(f"Unsupported expression type in mapping: {type(expression)}") + + @staticmethod + def _convert_root_link_mapping(root_lm: LinkMapping, osi_concept: Concept, entity_concept, ontology: OntologyModel, + properties_index: dict, reasoner: OntologyReasoner, + entity_bindings: dict | None = None) -> None: + entity_binding = OssieToRelationalAIConverter._entity_binding_for( + root_lm.object_mapping, osi_concept, entity_concept, ontology, reasoner, + entity_bindings if entity_bindings is not None else {}, + ) + if root_lm.relationship is not None: + # Unary relationship: the object_mapping identifies the entity and + # the relationship is asserted on it with no additional role. + pyrel_rel = properties_index[root_lm.relationship.full_name] + entity_binding.binds_to(pyrel_rel) + for child_lm in (root_lm.children or []): + OssieToRelationalAIConverter._apply_link_binding(child_lm, entity_binding, ontology, properties_index, reasoner) + + @staticmethod + def _apply_link_binding(lm: LinkMapping, entity_binding, ontology: OntologyModel, properties_index: dict, + reasoner: OntologyReasoner, intermediate_roles: list | None = None) -> None: + om = lm.object_mapping + + if lm.relationship is None: + # Intermediate role node for ternary (or higher arity) relationships. + # Build the role reference without emitting any binding, then forward + # it as accumulated context to each child. + role_val = OssieToRelationalAIConverter._build_role_value(om, ontology) + accumulated = (intermediate_roles or []) + ([role_val] if role_val is not None else []) + for child_lm in (lm.children or []): + OssieToRelationalAIConverter._apply_link_binding( + child_lm, entity_binding, ontology, properties_index, reasoner, accumulated + ) + return + + pyrel_rel = properties_index[lm.relationship.full_name] + + if intermediate_roles: + # Ternary (or higher): emit a single binding with all accumulated roles + this role. + final_val = OssieToRelationalAIConverter._build_role_value(om, ontology) + entity_binding.binds_to(pyrel_rel, *intermediate_roles, final_val) + # Continue into any children with a fresh intermediate context. + child_binding = entity_binding + elif om.referent_mappings: + # Binary entity→entity binding. + # binds_to_entity uses EntityMapping (filter_by), so no ghost entity is created here. + # Only create a child EntityBinding (creator) when there are grandchildren that need it. + kwargs = OssieToRelationalAIConverter._build_entity_kwargs(om.referent_mappings, ontology) + entity_binding.binds_to_entity(pyrel_rel, **kwargs) + if lm.children: + target_concept = OssieToRelationalAIConverter._concept(ontology, lm.relationship.last_role.player.name) + stype = OssieToRelationalAIConverter._concept(ontology, om.concept.name) if om.concept is not None else None + child_binding = ontology.EntityBinding(target_concept, stype=stype, **kwargs) + else: + child_binding = entity_binding + elif om.expression is not None: + pyrel_concept = ontology.lookup_concept(om.concept.name) if (om.concept is not None and not om.concept.is_builtin) else None + value = OssieToRelationalAIConverter._render_expression(om.expression, ontology, concept=pyrel_concept) + entity_binding.binds_to(pyrel_rel, value) + if lm.children: + # For entity targets with a single-element ref scheme, build an + # explicit child binding so grandchildren are scoped to that entity. + target_osi = lm.relationship.last_role.player + scheme = reasoner.ref_scheme(target_osi) + if scheme and len(scheme) == 1: + target_pyrel = OssieToRelationalAIConverter._concept(ontology, target_osi.name) + stype = OssieToRelationalAIConverter._concept(ontology, om.concept.name) if om.concept is not None else None + key_value = OssieToRelationalAIConverter._render_expression(om.expression, ontology) + child_binding = ontology.EntityBinding(target_pyrel, stype=stype, **{scheme[0].name: key_value}) + else: + child_binding = entity_binding + else: + child_binding = entity_binding + else: + child_binding = entity_binding + + for child_lm in (lm.children or []): + OssieToRelationalAIConverter._apply_link_binding(child_lm, child_binding, ontology, properties_index, reasoner) + + @staticmethod + def _build_role_value(om: ObjectMapping, ontology: OntologyModel): + """Build a PyRel ref or value from an object_mapping without emitting any binding rule. + + Used for intermediate (no-relationship) nodes in ternary link_mapping trees, where the + role value must be accumulated before the final relationship binding is emitted.""" + if om.referent_mappings: + kwargs = OssieToRelationalAIConverter._build_entity_kwargs(om.referent_mappings, ontology) + assert om.concept is not None, "a referent mapping must name its concept" + target = OssieToRelationalAIConverter._concept(ontology, om.concept.name) + return EntityMapping(target, **kwargs) + if om.expression is not None: + pyrel_concept = ontology.lookup_concept(om.concept.name) if (om.concept is not None and not om.concept.is_builtin) else None + return OssieToRelationalAIConverter._render_expression(om.expression, ontology, concept=pyrel_concept) + return None + + @staticmethod + def _build_entity_kwargs(referent_mappings: list[ReferentMapping], ontology: OntologyModel) -> dict[str, Any]: + """Identifier kwargs for an entity binding: relationship name -> value. + + The values are whatever the mapping renders to — a nested EntityMapping, + a pyrel Chain over a dataset column, or a ValueMapping — so `Any` is the + honest element type rather than a union that would have to be restated + every time the renderer learns a new shape. + """ + kwargs: dict[str, Any] = {} + for rm in referent_mappings: + rel_name = rm.relationship.name + if rm.referent_mappings: + target_concept = OssieToRelationalAIConverter._concept(ontology, rm.relationship.last_role.player.name) + sub_kwargs = OssieToRelationalAIConverter._build_entity_kwargs(rm.referent_mappings, ontology) + kwargs[rel_name] = EntityMapping(target_concept, **sub_kwargs) + elif rm.expression is not None: + kwargs[rel_name] = OssieToRelationalAIConverter._render_expression(rm.expression, ontology) + return kwargs + + # ------------------------------------------------------------------ + # Formulas / rules + # ------------------------------------------------------------------ + + @staticmethod + def _convert_formulas(oc: OntologyComponent, ontology: OntologyModel, reasoner: OntologyReasoner) -> None: + # `derived_by` does not guarantee a *parsed* formula. `FormulaFactory` + # hands back a plain `ossie_ontology.model.Formula` — raw text, no AST — + # and owl2osi builds them that way on purpose: OWL's formula vocabulary + # is not what the formula validator models. Wiring a FormulaParserFactory + # into owl2osi was tried and fails every OWL shape, not just one: + # + # IRI.names("", CodeListElement) -> Unsupported argument for the + # fact reference (derivations) + # EXISTS( IRI.names(...) ) -> Undefined relationship 'IRI.names' + # CatalogRecord.primary_topic -> Undefined relationship + # + # Only the enriched `Formula` imported above carries what FormulaConverter + # needs, so anything else is skipped: the rule goes undeclared, as it + # already did, rather than aborting the whole conversion. IATA alone + # reaches here with 81 such formulas, all OWL code-list enumerations. + var_name_cache = NameCache() + for concept in oc.concepts(exclude_builtin=True): + if concept.derived_by: + for formula in concept.derived_by: + if isinstance(formula, Formula): + FormulaConverter(oc, ontology, reasoner, var_name_cache, formula) + for rel in oc.relationships: + if rel.derived_by: + for formula in rel.derived_by: + if isinstance(formula, Formula): + FormulaConverter(oc, ontology, reasoner, var_name_cache, formula) + + # ------------------------------------------------------------------ + # Reading string generation + # ------------------------------------------------------------------ + + @staticmethod + def _generate_reading(rel: Relationship, ontology: OntologyModel) -> str: + verbalizations = rel.verbalizations + if not verbalizations: + return rel.name + parts = [] + for role in verbalizations[0].roles: + if role.preceding_text: + parts.append(role.preceding_text) + if role.prefix: + parts.append(role.prefix) + pyrel_concept = OssieToRelationalAIConverter._concept(ontology, role.concept.name) + if role.name: + parts.append(format(pyrel_concept, role.name)) + else: + parts.append(f"{pyrel_concept}") + if role.postfix: + parts.append(role.postfix) + if role.following_text: + parts.append(role.following_text) + return " ".join(parts) + + # ------------------------------------------------------------------ + # Helpers + # ------------------------------------------------------------------ + + @staticmethod + def _sort_dependency_graph(concepts: list[Concept]) -> list[str]: + from ossie_ontology.common.graph import topological_sort + from relationalai.semantics.frontend.base import CoreConcepts + nodes = [] + edges = [] + for concept in concepts: + name = concept.name + nodes.append(name) + for parent in concept.extends: + if parent.name not in CoreConcepts: + edges.append((parent.name, name)) + return topological_sort(nodes, edges) diff --git a/converters/ontology/src/ossie_ontology/converter/palantir_to_ossie/converter.py b/converters/ontology/src/ossie_ontology/converter/palantir_to_ossie/converter.py index c87ef7ac..06debc47 100644 --- a/converters/ontology/src/ossie_ontology/converter/palantir_to_ossie/converter.py +++ b/converters/ontology/src/ossie_ontology/converter/palantir_to_ossie/converter.py @@ -24,7 +24,7 @@ from ossie_ontology.common.graph import topological_sort_break_cycles from ossie_ontology.common.utils import to_pascal_case, to_verbalization_string -from ossie_ontology.external.palantir.model import ( +from ossie_ontology.vendor.palantir.model import ( ArrayDataType, DataSet as PalantirDataSet, DataSetColumn, @@ -138,7 +138,10 @@ def _allowed(resource: Resource, statuses: frozenset) -> bool: return resource.status() in statuses def __init__(self, formula_factory: FormulaFactory | None = None): - self._formula_factory = formula_factory or FormulaFactory() + # See OssieParser: parsing is the default, the raw factory is the opt-out. + from ossie_ontology.expr.factory import FormulaParserFactory + + self._formula_factory = formula_factory or FormulaParserFactory() # ------------------------------------------------------------------ # Entry point diff --git a/converters/ontology/src/ossie_ontology/converter/spec_to_ossie/converter.py b/converters/ontology/src/ossie_ontology/converter/spec_to_ossie/converter.py index 0b45ea8a..dbeb6a47 100644 --- a/converters/ontology/src/ossie_ontology/converter/spec_to_ossie/converter.py +++ b/converters/ontology/src/ossie_ontology/converter/spec_to_ossie/converter.py @@ -22,6 +22,7 @@ import re from ossie_ontology.common.graph import topological_sort +from ossie_ontology.reasoner import OntologyReasoner from ossie_ontology.model import ( Concept, ConceptMapping, @@ -88,8 +89,11 @@ class SpecToOssieConverter: def __init__(self, formula_factory: FormulaFactory | None = None, mapping_formula_factory: MappingFormulaFactory | None = None): - self._formula_factory = formula_factory or FormulaFactory() - self._mapping_formula_factory = mapping_formula_factory or MappingFormulaFactory() + # See OssieParser: parsing is the default, the raw factory is the opt-out. + from ossie_ontology.expr.factory import FormulaParserFactory, MappingFormulaParserFactory + + self._formula_factory = formula_factory or FormulaParserFactory() + self._mapping_formula_factory = mapping_formula_factory or MappingFormulaParserFactory() def convert(self, spec: OssieSpec) -> OssieOntology: ontology = OntologyComponent() @@ -300,7 +304,10 @@ def _convert_object_mapping( ) expression: DatasetField | Formula | None = None if om_spec.expression is not None: - expression = self._resolve_mapping_expression(om_spec.expression, semantic_model, concept, ontology) + expression = self._resolve_mapping_expression( + om_spec.expression, semantic_model, + SpecToOssieConverter._expression_type(concept if concept is not None else container), ontology, + ) referent_mappings = None if om_spec.referent_mappings is not None: rm_container = concept if concept is not None else container @@ -344,7 +351,6 @@ def _convert_link_mapping( container: Concept, lm_spec: SpecLinkMapping, ) -> LinkMapping: - object_mapping = self._convert_object_mapping(model, ontology, semantic_model, container, lm_spec.object_mapping) relationship: Relationship | None = None if lm_spec.relationship is not None: relationship = ontology.lookup_concept_relationship(container, lm_spec.relationship) @@ -353,15 +359,38 @@ def _convert_link_mapping( f"LinkMapping references unknown relationship " f"'{container.name}.{lm_spec.relationship}' in ontology '{model.name}'." ) + # Resolved first: the object_mapping below describes the *target* of the + # relationship, not the container it was looked up on, and it needs that + # target to know what a bare `expression:` holds. + target = relationship.last_role.player if relationship is not None else container + object_mapping = self._convert_object_mapping(model, ontology, semantic_model, target, lm_spec.object_mapping) children: list[LinkMapping] | None = None if lm_spec.children is not None: - child_container = relationship.last_role.player if relationship is not None else container + child_container = target children = [ self._convert_link_mapping(model, ontology, semantic_model, child_container, child) for child in lm_spec.children ] return LinkMapping(object_mapping=object_mapping, relationship=relationship, children=children) + @staticmethod + def _expression_type(target: Concept) -> Concept | None: + """The concept a bare `expression:` on an object mapping actually holds. + + For a value type that is the type itself. For an entity the expression + is not the entity — it is what identifies it — so the type is the player + of its identifying relationship, which is what + `_convert_referent_mapping` already reads off `rel.last_role.player`. + A composite reference scheme cannot be keyed by one expression at all; + `_build_entity_binding` reports that, so pin nothing and let it. + """ + scheme = OntologyReasoner.ref_scheme(target) + if scheme is None: + return target + if len(scheme) != 1: + return None + return scheme[0].last_role.player + # ----- Formula helpers ----------------------------------------------- def _build_rule(self, raw: str | None, parent: Container | None, ontology: OntologyComponent) -> Formula | None: diff --git a/converters/ontology/src/ossie_ontology/expr/__init__.py b/converters/ontology/src/ossie_ontology/expr/__init__.py new file mode 100644 index 00000000..52ff31de --- /dev/null +++ b/converters/ontology/src/ossie_ontology/expr/__init__.py @@ -0,0 +1,45 @@ +# 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. + + +def acceptor(cls): + """ + A decorator that adds an `accept` method to the class. + The `accept` method calls the appropriate handler method + of a Dispatcher. + + Using a decorator here allows us to compute the handler name once + for the class rather than each time we dispatch. + """ + attr_name = f"visit_{cls.__name__.lower()}" + + def accept(self, v, parent=None): + return getattr(v, attr_name)(self, parent) + + cls.accept = accept + return cls + + +class Node: + """Represents a node in an Expression tree""" + + @property + def kind(self): + return self.__class__.__name__.lower() + + def accept(self, v, parent=None): + raise NotImplementedError(f"accept not implemented for {self.kind}") \ No newline at end of file diff --git a/converters/ontology/src/ossie_ontology/expr/common.py b/converters/ontology/src/ossie_ontology/expr/common.py new file mode 100644 index 00000000..bfccca2d --- /dev/null +++ b/converters/ontology/src/ossie_ontology/expr/common.py @@ -0,0 +1,39 @@ +# 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 __future__ import annotations +from enum import Enum + + +class AggMethod(str, Enum): + SUM = "SUM" + MIN = "MIN" + MAX = "MAX" + COUNT = "COUNT" + AVG = "AVG" + + @classmethod + def from_value(cls, value) -> AggMethod: + """ + Look up an enum instance by a string value (case-insensitive). + """ + if not isinstance(value, str): + raise TypeError("value must be a string") + try: + return cls(value.strip().upper()) + except ValueError as e: + raise ValueError(f"Unknown aggregation method: {value}") from e diff --git a/converters/ontology/src/ossie_ontology/expr/factory.py b/converters/ontology/src/ossie_ontology/expr/factory.py new file mode 100644 index 00000000..2f0fb16f --- /dev/null +++ b/converters/ontology/src/ossie_ontology/expr/factory.py @@ -0,0 +1,193 @@ +# 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. + +"""The factories that wire the expression parser into the model's formula hooks. + +`ossie_ontology.model` declares `FormulaFactory` and `MappingFormulaFactory` as +hook points that keep a formula's raw text and nothing else. The two below fill +them in: they lex and parse the expression into an AST and validate it against +the ontology (and, for a mapping expression, against the semantic model's +columns). They are what `OssieParser`, `SpecToOssieConverter` and +`PalantirToOssieConverter` use unless a caller passes something else. + +They sit under `expr` rather than `parser` because that is all they depend on -- +`parser` imports `SpecToOssieConverter`, which would make a factory import from +there a cycle. +""" + +from collections import OrderedDict +from typing import Callable + +from ossie_ontology.expr.formula.model import ( + Var, + Formula, + ConceptRefHandle, + VarHandle +) +from ossie_ontology.expr.formula.parser import FormulaParser +from ossie_ontology.expr.formula.visitor.collector import FormulaCollector +from ossie_ontology.model import ( + Concept, + Relationship, + Role, + OntologyComponent, + FormulaFactory, + MappingFormulaFactory, + FormulaParent, + Formula as OSIFormula, + SemanticModel +) +from ossie_ontology.expr.formula.validator import FormulaValidator, MappingFormulaValidator +from ossie_ontology.reasoner import OntologyReasoner + +Container = Concept | Relationship +# What a formula can be scoped to: a concept, a relationship, the +# (concept, relationship) pair an identifier rule carries, or nothing at all for +# an ontology-level `requires`, which has no parent scope to bind against. +FormulaScope = Concept | Relationship | tuple[Concept, Relationship] | None + + +class FormulaParserFactory(FormulaFactory): + """FormulaFactory that parses raw expressions into enriched model.Formula + instances carrying a full AST, expression info, and head variables. + + Parsers are cached by ontology identity since FormulaParser construction + is expensive (yacc compilation). Safe to reuse across multiple convert() calls. + + When parent is None (mapping expression context) falls back to a plain Formula. + """ + + def __init__(self, var_extract_fn: Callable[[FormulaScope], OrderedDict[Var, Concept]] | None = None): + self._var_extract_fn = var_extract_fn + self._parsers: dict[int, FormulaParser] = {} + self._reasoners: dict[int, OntologyReasoner] = {} + + def __call__( + self, + raw_expr: str, + parent: FormulaParent = None, + ontology: OntologyComponent | None = None, + ) -> OSIFormula: + # With neither a parent nor an ontology there is no context to resolve + # references against, so fall back to a plain, unparsed formula. + if parent is None and ontology is None: + return super().__call__(raw_expr=raw_expr, parent=parent, ontology=ontology) + if ontology is None: + raise ValueError("FormulaParserFactory requires an ontology to parse rule formulas.") + # parent may still be None here: ontology-level `requires` constraints + # (e.g. "COUNT[Airport] > 0") are not scoped to any concept/relationship. + # They have no head vars, but are still parsed and validated against the ontology. + var_extract_fn = self._var_extract_fn or extract_head_vars + key = id(ontology) + if key not in self._parsers: + self._parsers[key] = FormulaParser(ontology) + if key not in self._reasoners: + reasoner = OntologyReasoner() + ontology.register(reasoner) + self._reasoners[key] = reasoner + return parse_raw_formula(ontology, self._reasoners[key], raw_expr, parent, self._parsers[key], var_extract_fn) + + +class MappingFormulaParserFactory(MappingFormulaFactory): + """MappingFormulaFactory that parses raw mapping expressions into enriched + model.Formula instances, validated with MappingFormulaValidator. + + Both ontology and semantic_model are mandatory — field references in mapping + expressions must be resolved against the semantic model at parse time. + Parsers are cached per (ontology, semantic_model) pair. + """ + + def __init__(self): + self._parsers: dict[tuple[int, int], FormulaParser] = {} + + def __call__( + self, + raw_expr: str, + parent: FormulaParent = None, + ontology: OntologyComponent | None = None, + semantic_model: SemanticModel | None = None, + ) -> OSIFormula: + if ontology is None: + raise ValueError("MappingFormulaParserFactory requires an ontology.") + if semantic_model is None: + raise ValueError("MappingFormulaParserFactory requires a semantic_model.") + key = (id(ontology), id(semantic_model)) + if key not in self._parsers: + self._parsers[key] = FormulaParser(ontology, semantic_model) + parser = self._parsers[key] + expr = parser.parse_formula(raw_expr) + collector = FormulaCollector({}) + expr.accept(collector) + MappingFormulaValidator.validate(collector.expression_info(), raw_expr) + return Formula([expr], collector.expression_info(), OrderedDict(), raw_expr, parent) + + +def extract_head_vars(parent: FormulaScope) -> OrderedDict[Var, Concept]: + head_vars: OrderedDict[Var, Concept] = OrderedDict() + + # Ontology-level `requires` constraints have no parent scope and therefore + # no head vars to bind. + if parent is None: + return head_vars + + # The pair form is keyed by its relationship, matching how the tuple branch + # below reads it; `hasattr` would have silently fallen through to repr(). + scope = parent[1] if isinstance(parent, tuple) else parent + + def _add_head_var(role: Role): + if not role.explicit_name: + return + vh = VarHandle(role.explicit_name) + if vh in head_vars: + if head_vars[vh] != role.player: + raise ValueError( + f"An ambiguous type for the head var '{vh.name()}' for " + f"'{scope.name}': " + f"'{vh.name()}' and '{head_vars[vh].name}'" + ) + else: + head_vars[vh] = role.player + + if isinstance(parent, Concept): + for rel in parent.identify_by.values(): + _add_head_var(rel.last_role) + elif isinstance(parent, Relationship): + for role in parent.roles: + _add_head_var(role) + elif isinstance(parent, tuple): + _, rel = parent + for role in rel.roles: + _add_head_var(role) + + return head_vars + + +def parse_raw_formula( + ontology: OntologyComponent, + reasoner: OntologyReasoner, + raw_formula: str, + parent: FormulaScope, + parser: FormulaParser, + var_extract_fn: Callable[[FormulaScope], OrderedDict[Var, Concept]], +) -> Formula: + head_vars = var_extract_fn(parent) + expr = parser.parse_formula(raw_formula, head_vars) + collector = FormulaCollector(head_vars) + expr.accept(collector) + formula = Formula([expr], collector.expression_info(), head_vars, raw_formula, parent) + FormulaValidator.validate_rule(ontology, reasoner, formula) + return formula \ No newline at end of file diff --git a/converters/ontology/src/ossie_ontology/expr/formula/__init__.py b/converters/ontology/src/ossie_ontology/expr/formula/__init__.py new file mode 100644 index 00000000..24569233 --- /dev/null +++ b/converters/ontology/src/ossie_ontology/expr/formula/__init__.py @@ -0,0 +1,17 @@ +# 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. + diff --git a/converters/ontology/src/ossie_ontology/expr/formula/lexer.py b/converters/ontology/src/ossie_ontology/expr/formula/lexer.py new file mode 100644 index 00000000..208577cc --- /dev/null +++ b/converters/ontology/src/ossie_ontology/expr/formula/lexer.py @@ -0,0 +1,126 @@ +# 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 ply.lex as lex + +class FormulaLexer: + + def __init__(self): + self.lexer = lex.lex(module=self) + self.lexer.begin('INITIAL') + + reserved = { + 'AND': 'AND', + 'AVG': 'AVG', + 'BY' : 'BY', + 'COUNT': 'COUNT', + 'EXISTS': 'EXISTS', + 'FALSE': 'FALSE', + 'GROUP': 'GROUP', + 'MAX' : 'MAX', + 'MIN' : 'MIN', + 'NOT': 'NOT', + 'OR': 'OR', + 'SUM': 'SUM', + 'TRUE': 'TRUE', + 'WHERE': 'WHERE' + } + + tokens = [ + 'ID', + 'COMMA', + 'DIVIDE', + 'DOT', + 'EQUALS', + 'GREATER', + 'GREATEREQ', + 'LBRACKET', + 'LESS', + 'LESSEQ', + 'LPAREN', + 'MINUS', + 'INTEGER', + 'FLOAT', + 'NOTEQUALS', + 'PLUS', + 'RBRACKET', + 'RPAREN', + 'STRING_LITERAL', + 'TIMES' + ] + list(reserved.values()) + + # Ignored characters + t_ignore = ' \t' + + # Token regular expressions + t_COMMA = r',' + t_DIVIDE = r'/' + t_DOT = r'\.' + t_EQUALS = r'==' + t_GREATER = r'>' + t_LBRACKET = r'\[' + t_LESS = r'<' + t_LPAREN = r'\(' + t_MINUS = r'\-' + t_PLUS = r'\+' + t_RBRACKET = r'\]' + t_RPAREN = r'\)' + t_TIMES = r'\*' + + def t_GREATEREQ(self, t): + r'>=' + t.type = 'GREATEREQ' + return t + + def t_LESSEQ(self, t): + r'<=' + t.type = 'LESSEQ' + return t + + def t_NEWLINE(self, t): + r'\n+' + t.lexer.lineno += t.value.count('\n') + + def t_NOTEQUALS(self, t): + r'!=' + t.type = 'NOTEQUALS' + return t + + def t_INTEGER(self, t): + r'\d+(?!\.\d)' + t.value = int(t.value) + return t + + def t_FLOAT(self, t): + r'\d+\.\d+' + t.value = float(t.value) + return t + + def t_STRING_LITERAL(self, t): + r'\'[^\']*\'' + t.value = t.value[1:-1] + return t + + def t_ID(self, t): + r'[a-zA-Z_0-9]+' + t.type = self.reserved.get(t.value, 'ID') + return t + + # every string that doesn't match one of the previous tokens is considered an error + def t_error(self, t): + r'.' + return SyntaxError(f'Illegal character {t.value!r} at position {t.lexpos}') diff --git a/converters/ontology/src/ossie_ontology/expr/formula/model.py b/converters/ontology/src/ossie_ontology/expr/formula/model.py new file mode 100644 index 00000000..df88fb20 --- /dev/null +++ b/converters/ontology/src/ossie_ontology/expr/formula/model.py @@ -0,0 +1,410 @@ +# 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. + +"""Formula AST handle types.""" +from __future__ import annotations + +from collections import OrderedDict +from collections.abc import Hashable +from dataclasses import dataclass, field +from ossie_ontology.expr import acceptor, Node +from ossie_ontology.expr.common import AggMethod + +from ossie_ontology.model import ( + Concept, + Relationship, + Dataset, + DatasetField, + FormulaParent, + Formula as OSIFormula, +) + +from ossie_ontology.expr.model import Expression as ExprNode + + +@acceptor +class AggregationMethod(Node): + def __init__(self, val: AggMethod): + self._val = val + + def __str__(self): + return str(self._val) + + def __repr__(self): + return f"AggregationMethod{{({self.__str__()})}}" + + +@acceptor +class AggExpr(Node): + + def __init__(self, meth: AggregationMethod, vseq, formula, grp_by): + self._method = meth + self._var_seq = vseq + self._formula = formula + self._group_by = grp_by + + def __str__(self): + vars = ', '.join([str(var) for var in self._var_seq]) + where = f" WHERE {self._formula}" if self._formula is not None else "" + grp_by = f" GROUP BY {', '.join(str(var) for var in self._group_by)}" if self._group_by else "" + return f"{self._method} [{vars}{where}{grp_by}]" + + def __repr__(self): + return f"AggExpr{{({self.__str__()})}}" + + +@acceptor +class Negation(Node): + def __init__(self, arg): + self._arg = arg + + def arg(self): + return self._arg + + def __hash__(self): + return hash(("NOT", self._arg.__hash__())) + + def __str__(self): + return f"not ({str(self._arg)})" + + def __repr__(self): + return f"Not{{({self._arg.__repr__()})}}" + + +@acceptor +class Conjunction(Node): + def __init__(self, left, right): + self._left = left + self._right = right + + def left(self): + return self._left + + def right(self): + return self._right + + def __hash__(self): + return hash(("AND", self._left.__hash__(), self._right.__hash__())) + + def __str__(self): + return f"({self._left} AND {self._right})" + + def __repr__(self): + return f"Conjunction{{{self._left.__repr__()} AND {self._right.__repr__()}}}" + + +@acceptor +class Disjunction(Node): + def __init__(self, left, right): + self._left = left + self._right = right + + def left(self): + return self._left + + def right(self): + return self._right + + def __hash__(self): + return hash(("OR", self._left.__hash__(), self._right.__hash__())) + + def __str__(self): + return f"({self._left} OR {self._right})" + + def __repr__(self): + return f"Disjunction{{{self._left.__repr__()} OR {self._right.__repr__()}}}" + + +@acceptor +class Exists(Node): + def __init__(self, body): + self._body = body + + def body(self): + return self._body + + def __hash__(self): + return hash(("EXISTS", self._body.__hash__())) + + def __str__(self): + return f"EXISTS ({self._body})" + + def __repr__(self): + return f"Exists{{{self._body.__repr__()}}}" + + +@acceptor +class WhereExpr(Node): + def __init__(self, expr, formula): + self._expr = expr + self._formula = formula + + def expr(self): + return self._expr + + def formula(self): + return self._formula + + def __hash__(self): + return hash(("WHERE", self._expr.__hash__(), self._formula.__hash__())) + + def __str__(self): + return f"({self._expr} WHERE ({self._formula}))" + + def __repr__(self): + return f"WhereExpr{{{self._expr.__repr__()} WHERE {self._formula.__repr__()}}}" + + +@acceptor +class Handle(Node): + """Base for the leaf nodes of a formula AST. + + Identity is declared once per subclass by `_key`, and both `__eq__` and + `__hash__` are derived from it. Keeping them on one definition is what makes + them agree: a subclass that hashes its fields while equality compares + something else puts equal objects in different buckets, and a dict lookup + then misses the entry it just stored. + """ + + def _key(self) -> Hashable: + """What identifies this handle. Subclasses override with their fields. + + Annotated as `Hashable` rather than left to inference: several subclasses + return a tuple, and inferring `str` from this default would make every + one of those an incompatible override. The only real requirement is that + whatever comes back can be hashed and compared. + """ + return str(self) + + def __eq__(self, other): + if isinstance(other, self.__class__): + return self._key() == other._key() + return False + + def __hash__(self): + return hash(self._key()) + + +@acceptor +class Atom(Handle): + pass + + +@acceptor +class ConceptRefHandle(Atom): + def __init__(self, concept: Concept): + self._concept = concept + + def __str__(self): + return f"{self._concept}" + + def __repr__(self): + return f"ConceptRefHandle{{{self}}}" + + +@acceptor +class RelationshipRefHandle(Atom): + def __init__(self, relationship: Relationship): + self._relationship = relationship + + def __str__(self): + return f"{self._relationship}" + + def __repr__(self): + return f"RelationshipRefHandle{{{self}}}" + + +@acceptor +class DatasetRefHandle(Atom): + def __init__(self, dataset: Dataset): + self._dataset = dataset + + def dataset(self): + return self._dataset + + def name(self): + return f"{self.__str__()}" + + def _key(self) -> Hashable: + return self._dataset.name + + def __str__(self): + return f"{self._dataset}" + + def __repr__(self): + return f"DatasetRefHandle{{{self}}}" + + +@acceptor +class DatasetFieldHandle(Atom): + def __init__(self, dataset: Dataset, field: DatasetField): + self._dataset = dataset + self._field = field + + def _key(self) -> Hashable: + # The dataset and field objects themselves are unhashable — both are + # dataclasses, so Python sets their `__hash__` to None — which made + # hashing this handle raise TypeError. Key on their names instead, with + # the dataset's source included because two distinct datasets can share + # a name (see `_expression_key` in the RelationalAI converter) while a + # bare column name is not unique across datasets at all. + return self._dataset.source, self._dataset.name, self._field.name + + def __str__(self): + return f"{self._field}" + + def __repr__(self): + return f"DatasetFieldHandle{{{self._dataset}.{self._field}}}" + + +@acceptor +class VarHandle(Atom): + def __init__(self, v): + self._var = v + + def name(self): + return self._var + + def _key(self) -> Hashable: + return self._var + + def __str__(self): + return str(self._var) + + def __repr__(self): + return f"VarHandle{{{self}}}" + + +@acceptor +class LiteralHandle(Atom): + def __init__(self, val): + self._val = val + + def _key(self) -> Hashable: + return self._val + + def __str__(self): + return str(self._val) + + def __repr__(self): + return f"LiteralHandle{{{self}}}" + + +@acceptor +class FactRef(Atom): + def __init__(self, handle: Atom, *args): + self._handle = handle + self._args = args + + def _key(self) -> Hashable: + return (self._handle, *self._args) + + def __str__(self): + return f"{self._handle}({', '.join([str(arg) for arg in self._args])})" + + def __repr__(self): + expr = f"{self._handle.__repr__()}( {', '.join(arg.__repr__() for arg in self._args)} )" + return f"FactRef{{{expr}}}" + + +@acceptor +class DotJoinHandle(Atom): + def __init__(self, handle1: Atom, handle2: Atom): + self._handle1 = handle1 + self._handle2 = handle2 + + def _key(self) -> Hashable: + return self._handle1, self._handle2 + + def __str__(self): + return f"{self._handle1}.{self._handle2}" + + def __repr__(self): + expr = f"{self._handle1.__repr__()} . {self._handle2.__repr__()}" + return f"DotJoinHandle{{ {expr} }}" + + def last_handle(self): + if isinstance(self._handle2, DotJoinHandle): + return self._handle2.last_handle() + return self._handle2 + + +# --------------------------------------------------------------------------- +# Formula +# --------------------------------------------------------------------------- + +@dataclass +class FormulaExpressionInfo: + var_handles: list[tuple[VarHandle, Concept | None]] = field(default_factory=list) + literal_handles: list[LiteralHandle] = field(default_factory=list) + concept_ref_handles: list[tuple[ConceptRefHandle, Concept]] = field(default_factory=list) + relationship_ref_handles: list[RelationshipRefHandle] = field(default_factory=list) + dataset_ref_handles: list[DatasetRefHandle] = field(default_factory=list) + dataset_field_handles: list[DatasetFieldHandle] = field(default_factory=list) + fact_refs: list[FactRef] = field(default_factory=list) + dot_join_handles: list[DotJoinHandle] = field(default_factory=list) + + +class Formula(OSIFormula): + _expr: list[ExprNode | Handle | Negation] + _expr_info: FormulaExpressionInfo + _head_vars: OrderedDict[VarHandle | ConceptRefHandle, Concept] + + def __init__( + self, + expr: list[ExprNode | Handle | Negation], + expr_info: FormulaExpressionInfo, + head_vars: OrderedDict[VarHandle | ConceptRefHandle, Concept], + raw_expr: str, + parent: FormulaParent, + ): + super().__init__(raw_expr, parent) + self._expr = expr + self._expr_info = expr_info + self._head_vars = head_vars + + def expr(self): + return self._expr + + def expr_info(self) -> FormulaExpressionInfo: + return self._expr_info + + def head_vars(self): + return self._head_vars + + def last_head_var(self): + return next(reversed(self._head_vars), None) + + def formula_target(self): + if len(self._expr) == 1 and isinstance( + self._expr[0], + (ExprNode, LiteralHandle, DotJoinHandle, AggExpr), + ): + last_head_var = self.last_head_var() + if last_head_var and self._head_vars[last_head_var].is_primitive: + return last_head_var, self._head_vars[last_head_var] + return None + + def is_formula_target(self, var) -> bool: + target = self.formula_target() + return target[0] == var if target else False + + +# What a formula variable can be named by: a plain var, or a concept reference +# standing in for one. Head vars are keyed by either, so anything holding them +# has to accept both. +Var = VarHandle | ConceptRefHandle diff --git a/converters/ontology/src/ossie_ontology/expr/formula/parser.py b/converters/ontology/src/ossie_ontology/expr/formula/parser.py new file mode 100644 index 00000000..bad4d0c3 --- /dev/null +++ b/converters/ontology/src/ossie_ontology/expr/formula/parser.py @@ -0,0 +1,335 @@ +# 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. + +"""Formula parser. The grammar, precedence table, and AST.""" + +from __future__ import annotations + +import ply.yacc as yacc + +from ossie_ontology.model import OntologyComponent, SemanticModel, Concept, Dataset +from ossie_ontology.expr.model import Expression, BinOp +from ossie_ontology.expr.formula.model import ( + Atom, + VarHandle, + LiteralHandle, + Negation, + Conjunction, + Disjunction, + Exists, + WhereExpr, + AggregationMethod, + AggExpr, + DotJoinHandle, + FactRef, + ConceptRefHandle, + RelationshipRefHandle, + DatasetRefHandle, + DatasetFieldHandle, +) +from ossie_ontology.expr.formula.lexer import FormulaLexer +from ossie_ontology.expr.common import AggMethod + + +class FormulaParser: + """Formula parser. Constructed against a single `Ontology` (where + concepts and relationships live). For mapping formulas, callers pass an + additional `SemanticModel` so dataset and field references resolve.""" + + _HEAD_VARS_PARAM = "head_vars" + _MAPPING_DATASET_PARAM = "mapping_dataset" + + def __init__(self, ontology: OntologyComponent, semantic_model: SemanticModel | None = None): + # `write_tables=False, debug=False`: ply's defaults write `parsetab.py` + # (~12 KB) and `parser.out` (~85 KB) next to this module, which for an + # installed package means into site-packages on first parse — it fails + # on a read-only install and dirties container layers. Rebuilding the + # LALR tables costs ~4 ms, so there is nothing to cache. + self.parser = yacc.yacc(module=self, debug=False, write_tables=False) + self.lexer = FormulaLexer() + self._ontology = ontology + self._semantic_model = semantic_model + + def with_semantic_model(self, semantic_model: SemanticModel) -> FormulaParser: + """Returns a parser bound to the same ontology and the given logical + model. Useful when iterating over multiple OntologyMappings.""" + return FormulaParser(self._ontology, semantic_model) + + tokens = FormulaLexer.tokens + + precedence = ( + ("left", "OR"), + ("left", "AND"), + ("right", "NOT"), + ("nonassoc", "EQUALS", "NOTEQUALS", "GREATER", "GREATEREQ", "LESS", "LESSEQ"), + ("left", "LPAREN"), + ("left", "DOT"), + ("left", "PLUS", "MINUS"), + ("left", "TIMES", "DIVIDE"), + ) + + # ----------------- grammar ------------------------------------------ + + def p_goal(self, p): + "goal : formula" + p[0] = p[1] + + def p_formula_expr(self, p): + "formula : expr" + p[0] = p[1] + + def p_formula_comp_expr(self, p): + "formula : expr comp_op expr" + op = BinOp.from_value(p[2]) + p[0] = Expression(op, p[1], p[3]) + + def p_formula_and(self, p): + "formula : formula AND formula" + p[0] = Conjunction(p[1], p[3]) + + def p_formula_or(self, p): + "formula : formula OR formula" + p[0] = Disjunction(p[1], p[3]) + + def p_formula_exists(self, p): + "formula : EXISTS LPAREN formula RPAREN" + p[0] = Exists(p[3]) + + def p_formula_not(self, p): + "formula : NOT formula" + p[0] = Negation(p[2]) + + def p_expr_node_expr(self, p): + "expr : node_expr" + p[0] = p[1] + + def p_expr_minus_expr(self, p): + "expr : MINUS expr %prec NOT" + p[0] = Expression(BinOp.MINUS, LiteralHandle(0), p[2]) + + def p_expr_agg(self, p): + "expr : agg_expr" + p[0] = p[1] + + def p_expr_arith(self, p): + """ + expr : expr PLUS expr + | expr MINUS expr + | expr TIMES expr + | expr DIVIDE expr + """ + op = BinOp.from_value(p[2]) + p[0] = Expression(op, p[1], p[3]) + + def p_expr_where(self, p): + "expr : expr WHERE LPAREN formula RPAREN" + p[0] = WhereExpr(p[1], p[4]) + + def p_expr_parenthesized(self, p): + "expr : LPAREN expr RPAREN" + p[0] = p[2] + + def p_node_expr_literal(self, p): + "node_expr : literal" + p[0] = p[1] + + def p_node_expr_variable(self, p): + "node_expr : ID" + mapping_dataset = p.parser.params.get(self._MAPPING_DATASET_PARAM) + if mapping_dataset is not None: + field = mapping_dataset.field(p[1]) + if field is not None: + p[0] = DatasetFieldHandle(mapping_dataset, field) + return + handle = self._to_identifier_ref(p[1]) + if isinstance(handle, VarHandle): + raise ValueError( + f"'{p[1]}' is not a field in dataset '{mapping_dataset.name()}' " + f"and is not a known concept, relationship, or dataset" + ) + p[0] = handle + else: + head_vars = p.parser.params.get(self._HEAD_VARS_PARAM) + if head_vars: + vh = VarHandle(p[1]) + if vh in head_vars: + p[0] = vh + return + p[0] = self._to_identifier_ref(p[1]) + + def p_node_expr_dot(self, p): + "node_expr : node_expr DOT ID" + left_handle = p[1] + step_name = p[3] + + # `Dataset.field` always means the dataset's field, even when the + # identifier on the right also matches a concept or relationship name + # in the ontology. Resolve the right side as a field of the dataset + # directly — never through _to_identifier_ref, which would shadow it + # with the ontology hit. + if isinstance(left_handle, DatasetRefHandle): + dataset = self._lookup_dataset(left_handle.name()) + if dataset is None: + raise ValueError(f"Unknown dataset '{left_handle.name()}'") + field = dataset.field(step_name) + if field is None: + raise ValueError( + f"Undefined column '{step_name}' for dataset '{left_handle.name()}'" + ) + p[0] = DotJoinHandle(left_handle, DatasetFieldHandle(dataset, field)) + return + + container: Concept | None = None + if isinstance(left_handle, ConceptRefHandle): + container = left_handle._concept + elif isinstance(left_handle, VarHandle): + head_vars = p.parser.params.get(self._HEAD_VARS_PARAM) + if head_vars and left_handle in head_vars: + container = head_vars[left_handle] + elif isinstance(left_handle, DotJoinHandle): + handle = left_handle._handle2 + if isinstance(handle, RelationshipRefHandle): + container = handle._relationship.last_role.player + elif isinstance(left_handle, FactRef): + handle = left_handle._handle + if isinstance(handle, RelationshipRefHandle): + container = handle._relationship.last_role.player + elif isinstance(handle, DotJoinHandle) and isinstance(handle._handle2, RelationshipRefHandle): + container = handle._handle2._relationship.last_role.player + elif isinstance(handle, ConceptRefHandle): + container = handle._concept + + right_handle = self._to_identifier_ref(step_name, container) + p[0] = DotJoinHandle(left_handle, right_handle) + + def p_node_expr_apply(self, p): + "node_expr : node_expr LPAREN expr_seq RPAREN %prec LPAREN" + handle = self._to_identifier_ref(p[1]) + p[0] = FactRef(handle, *p[3]) + + def p_expr_seq(self, p): + "expr_seq : expr" + p[0] = [p[1]] + + def p_expr_seq_many(self, p): + "expr_seq : expr_seq COMMA expr" + p[1].append(p[3]) + p[0] = p[1] + + def p_node_expr_seq_single(self, p): + "node_expr_seq : node_expr" + p[0] = [p[1]] + + def p_node_expr_seq_many(self, p): + "node_expr_seq : node_expr_seq COMMA node_expr" + p[1].append(p[3]) + p[0] = p[1] + + def p_agg_expr(self, p): + "agg_expr : agg_method LBRACKET node_expr_seq WHERE formula GROUP BY node_expr_seq RBRACKET" + p[0] = AggExpr(p[1], p[3], p[5], p[8]) + + def p_agg_expr_bare(self, p): + "agg_expr : agg_method LBRACKET node_expr_seq RBRACKET" + # Aggregate over a bare expression with no WHERE filter or GROUP BY, + # e.g. an ontology-level constraint like "COUNT[Airport] > 0". + p[0] = AggExpr(p[1], p[3], None, []) + + def p_agg_method(self, p): + """ + agg_method : AVG + | COUNT + | MAX + | MIN + | SUM + """ + p[0] = AggregationMethod(AggMethod.from_value(p[1])) + + def p_comp_op(self, p): + """ + comp_op : EQUALS + | NOTEQUALS + | LESS + | LESSEQ + | GREATER + | GREATEREQ + """ + p[0] = p[1] + + def p_literal(self, p): + """ + literal : INTEGER + | FLOAT + | STRING_LITERAL + """ + p[0] = LiteralHandle(p[1]) + + def p_literal_bool(self, p): + """ + literal : TRUE + | FALSE + """ + p[0] = LiteralHandle(p[1] == "TRUE") + + def p_error(self, p): + raise ValueError("Unable to parse:", p) + + # ----------------- identifier resolution ----------------------------- + + def _to_identifier_ref(self, val, container: Concept | None = None) -> Atom: + if isinstance(val, Atom): + return val + + concept = self._ontology.lookup_concept(val) + relationship = ( + self._ontology.lookup_concept_relationship(container, val) + if container + else None # all relationships live under a concept; can't resolve without one + ) + dataset = self._lookup_dataset(val) + + matches = [m for m in (concept, relationship, dataset) if m is not None] + assert len(matches) <= 1, ( + f"Conflicting identifier: {val}, must be unique across concepts, relationships or datasets" + ) + + if concept is not None: + return ConceptRefHandle(concept) + if relationship is not None: + return RelationshipRefHandle(relationship) + if dataset is not None: + return DatasetRefHandle(dataset) + return VarHandle(val) + + def _lookup_dataset(self, name: str) -> Dataset | None: + if self._semantic_model is None: + return None + return self._semantic_model.lookup_dataset(name) + + # ----------------- entrypoint ---------------------------------------- + + def parse_formula( + self, + formula: str, + head_vars: dict[VarHandle | ConceptRefHandle, Concept] | None = None, + mapping_dataset: Dataset | None = None, + ): + self.parser.params = { + self._HEAD_VARS_PARAM: head_vars, + self._MAPPING_DATASET_PARAM: mapping_dataset, + } + return self.parser.parse(formula, lexer=self.lexer.lexer) \ No newline at end of file diff --git a/converters/ontology/src/ossie_ontology/expr/formula/validator.py b/converters/ontology/src/ossie_ontology/expr/formula/validator.py new file mode 100644 index 00000000..ec6c58c0 --- /dev/null +++ b/converters/ontology/src/ossie_ontology/expr/formula/validator.py @@ -0,0 +1,330 @@ +# 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 typing import Optional + +from ossie_ontology.expr.formula.model import ( + Formula, + FormulaExpressionInfo, + ConceptRefHandle, + RelationshipRefHandle, + DatasetRefHandle, + DatasetFieldHandle, + VarHandle, + DotJoinHandle, + FactRef +) +from ossie_ontology.expr.formula.visitor.unbound import UnboundRefFinder +from ossie_ontology.model import ( + OntologyComponent, + Concept, + Relationship +) +from ossie_ontology.reasoner import OntologyReasoner + +# What a variable in a formula body can be bound to. It starts as the concept +# the collector inferred (or None where inference found nothing), and a dot join +# can later rebind it to the relationship it resolved to. +VarType = Concept | Relationship | None + + +class FormulaValidator: + """FormulaValidator — validates a parsed formula against a single Ontology + (and an optional SemanticModel for mapping context).""" + + @staticmethod + def validate_rule(ontology: OntologyComponent, reasoner: OntologyReasoner, formula: Formula | None): + if formula is None or not formula._expr: + return + # An ontology-level `requires` has no parent concept/relationship to bind + # variables against, so every variable and concept reference must be + # bound within the formula itself — by an aggregate or by EXISTS. + if formula.parent is None: + FormulaValidator._validate_no_unbound_refs(formula) + head_vars = formula.head_vars() + body_vars = formula.expr_info().var_handles + + head_var_map = {k: v for k, v in head_vars.items() if isinstance(k, VarHandle)} + body_var_handles = FormulaValidator._build_handle_maps(body_vars, formula.raw_expr) + + FormulaValidator._validate_dot_join( + ontology, reasoner, formula.expr_info().dot_join_handles, body_var_handles, formula.raw_expr + ) + if isinstance(formula.parent, Relationship) and not reasoner.is_identifier_relationship(formula.parent): + for vh in head_var_map.keys(): + if vh not in body_var_handles and not formula.is_formula_target(vh): + raise ValueError( + f"Var '{vh.name()}' in the head not found in the body of the formula '{formula.raw_expr}'" + ) + FormulaValidator._validate_unresolved_var_handles(body_var_handles, formula.raw_expr) + FormulaValidator._validate_fact_refs( + reasoner, + formula.expr_info().fact_refs, + formula.expr_info().dot_join_handles, + body_var_handles, + formula.raw_expr, + ) + + @staticmethod + def _validate_no_unbound_refs(formula: Formula) -> None: + names: list[str] = [] + for handle in UnboundRefFinder(formula).unbound_refs(): + name = handle.name() if isinstance(handle, VarHandle) else handle._concept.name + if name not in names: + names.append(name) + if not names: + return + rendered = ", ".join(f"'{n}'" for n in names) + raise ValueError( + f"Unbound reference(s) {rendered} in the ontology-level constraint " + f"'{formula.raw_expr}'. An ontology-level `requires` has no parent scope to " + f"bind variables against, so every reference must be bound within the formula — " + f"by an aggregate (e.g. COUNT[...]) or by wrapping it in EXISTS(...), " + f"e.g. 'EXISTS({formula.raw_expr})'." + ) + + @staticmethod + def _build_handle_maps( + var_handles: list[tuple[VarHandle, Concept | None]], formula: str + ) -> dict[VarHandle, VarType]: + var_handles_map: dict[VarHandle, VarType] = {} + for h, c in var_handles: + if h in var_handles_map and var_handles_map[h] != c: + seen = var_handles_map[h] + raise ValueError( + f"An ambiguous type for the var '{h.name()}' in '{formula}': " + f"'{c.name if c else None}' and '{seen.name if seen else None}'" + ) + var_handles_map[h] = c + return var_handles_map + + @staticmethod + def _validate_dot_join( + ontology: OntologyComponent, + reasoner: OntologyReasoner, + dot_join_handles: list[DotJoinHandle], + var_handles: dict[VarHandle, VarType], + formula: str, + ): + for h in dot_join_handles: + left, right = h._handle1, h._handle2 + if isinstance(right, VarHandle): + rel: Relationship | None = None + if isinstance(left, VarHandle) and left in var_handles: + owner = var_handles[left] + # A var already resolved to a relationship (by an earlier dot + # join) owns no further relationships to look up. + if isinstance(owner, Concept): + rel = ontology.lookup_concept_relationship(owner, right._var) + if rel: + var_handles[right] = rel + else: + if isinstance(left, DatasetRefHandle): + raise ValueError( + f"Undefined column '{right}' for dataset '{left}' in the formula '{formula}'" + ) + raise ValueError(f"Undefined relationship '{h}' in the formula '{formula}'") + + if isinstance(right, RelationshipRefHandle): + first_role_player = right._relationship.roles[0].player + if isinstance(left, ConceptRefHandle): + concept = left._concept + elif isinstance(left, DotJoinHandle): + last_handle = left.last_handle() + if isinstance(last_handle, RelationshipRefHandle): + concept = last_handle._relationship.last_role.player + else: + concept = (var_handles.get(last_handle) + if isinstance(last_handle, VarHandle) else None) + elif isinstance(left, FactRef): + handle = left._handle + if isinstance(handle, RelationshipRefHandle): + concept = handle._relationship.last_role.player + elif isinstance(handle, DotJoinHandle) and isinstance(handle._handle2, RelationshipRefHandle): + concept = handle._handle2._relationship.last_role.player + else: + concept = None + else: + concept = var_handles.get(left) if isinstance(left, VarHandle) else None + if concept != first_role_player and not ( + isinstance(concept, Concept) + and reasoner.in_subtype_closure(concept, first_role_player) + ): + raise ValueError( + f"Type mismatch for '{h}' in the formula '{formula}'. " + f"Expected type in the relationship '{first_role_player}' got '{concept}'" + ) + + @staticmethod + def _validate_unresolved_var_handles(var_handles: dict[VarHandle, VarType], formula: str): + for k, v in var_handles.items(): + if v is None: + raise ValueError( + f"Unresolved reference '{k}' in the formula '{formula}'. " + f"If '{k}' is a dataset field, use the format 'DATASET_NAME.{k}'." + ) + + @staticmethod + def _validate_fact_refs( + reasoner: OntologyReasoner, + fact_refs: list[FactRef], + dot_join_handles: list[DotJoinHandle], + var_handles: dict[VarHandle, VarType], + formula: str, + ): + partial_applies = {dj._handle1 for dj in dot_join_handles if isinstance(dj._handle1, FactRef)} + for f in fact_refs: + # `Relationship` is reachable: a dot join can rebind a body var to + # the relationship it resolved to. `None` is not — the pass above + # rejects unresolved vars before this one runs. + args_type: list[Concept | Relationship] = [] + if isinstance(f._handle, RelationshipRefHandle): + rel = f._handle._relationship + elif isinstance(f._handle, DotJoinHandle) and isinstance(f._handle._handle2, RelationshipRefHandle): + rel = f._handle._handle2._relationship + if len(f._args) != rel.arity: + h1 = f._handle._handle1 + if isinstance(h1, ConceptRefHandle): + args_type.append(h1._concept) + elif isinstance(h1, VarHandle): + bound = var_handles[h1] + # None is unreachable here (see args_type above), but a + # dropped element would shift the arity check below, so + # only a real binding is contributed. + if bound is not None: + args_type.append(bound) + elif isinstance(h1, FactRef): + inner = h1._handle + if isinstance(inner, DotJoinHandle) and isinstance(inner._handle2, RelationshipRefHandle): + args_type.append(inner._handle2._relationship.last_role.player) + elif isinstance(inner, RelationshipRefHandle): + args_type.append(inner._relationship.last_role.player) + else: + continue + for arg in f._args: + t: Concept | Relationship | None = None + if isinstance(arg, ConceptRefHandle): + t = arg._concept + elif isinstance(arg, VarHandle): + t = var_handles[arg] + elif isinstance(arg, DotJoinHandle): + if isinstance(arg._handle1, RelationshipRefHandle): + t = arg._handle1._relationship.first_role.player + elif isinstance(arg._handle1, DatasetRefHandle) and isinstance(arg._handle2, DatasetFieldHandle): + field = arg._handle2._field + if field.type is None: + raise ValueError( + f"Dataset field '{arg._handle1.name()}.{field.name}' has no type " + f"and cannot be used as a typed argument in '{formula}'" + ) + t = field.type + elif isinstance(arg._handle2, RelationshipRefHandle): + t = arg._handle2._relationship.last_role.player + args_type.append(t) + if f in partial_applies: + args_type.append(rel.last_role.player) + continue + elif isinstance(arg, FactRef): + if isinstance(arg._handle, ConceptRefHandle): + t = arg._handle._concept + if t is None: + raise ValueError( + f"Unsupported argument '{arg}' for the fact reference in the formula '{formula}'" + ) + args_type.append(t) + + # A fact on the left of a dot join may leave its last role open and + # continue the traversal from it, so the dot join supplies that + # argument: 'Person.employment(Company).level' fills (Person, Company) + # and reads `level` off the Position playing the trailing role. The + # parser resolves the step after the dot the same way. + if f in partial_applies and len(args_type) == rel.arity - 1: + args_type.append(rel.last_role.player) + + rel_signature = rel.signature + mismatch = len(rel_signature) != len(args_type) or any( + expected != actual + and not (isinstance(actual, Concept) + and reasoner.in_subtype_closure(actual, expected)) + for expected, actual in zip(rel_signature, args_type) + ) + if mismatch: + raise ValueError( + f"Relationship '{rel}' signature mismatch in the formula '{formula}': " + f"expected ({', '.join(c.name for c in rel_signature)}) " + f"got ({', '.join(c.name for c in args_type)})" + ) + + +class MappingFormulaValidator: + """Validates a parsed mapping expression. + + Mapping expressions may only contain: dataset field references (qualified or + single-field), literals, and arithmetic/comparison operators. All ontology + constructs and unsupported expression types are rejected here so they fail + at spec-parse time rather than later during PyRel conversion.""" + + @staticmethod + def validate(expr_info: FormulaExpressionInfo, formula_str: str) -> None: + MappingFormulaValidator._validate_no_var_handles(expr_info, formula_str) + MappingFormulaValidator._validate_no_relationship_refs(expr_info, formula_str) + MappingFormulaValidator._validate_no_concept_refs(expr_info, formula_str) + MappingFormulaValidator._validate_no_dataset_refs(expr_info, formula_str) + MappingFormulaValidator._validate_no_fact_refs(expr_info, formula_str) + + @staticmethod + def _validate_no_var_handles(expr_info: FormulaExpressionInfo, formula_str: str) -> None: + for vh, _ in expr_info.var_handles: + raise ValueError( + f"Unresolved variable '{vh.name()}' in mapping expression '{formula_str}'. " + f"Use the qualified form 'DATASET_NAME.{vh.name()}' to reference a dataset field." + ) + + @staticmethod + def _validate_no_relationship_refs(expr_info: FormulaExpressionInfo, formula_str: str) -> None: + for rrh in expr_info.relationship_ref_handles: + raise ValueError( + f"Relationship references are not allowed in mapping expressions: " + f"'{rrh._relationship.name}' in '{formula_str}'." + ) + + @staticmethod + def _validate_no_concept_refs(expr_info: FormulaExpressionInfo, formula_str: str) -> None: + for crh, _ in expr_info.concept_ref_handles: + raise ValueError( + f"Concept references are not allowed in mapping expressions: " + f"'{crh._concept.name}' in '{formula_str}'." + ) + + @staticmethod + def _validate_no_dataset_refs(expr_info: FormulaExpressionInfo, formula_str: str) -> None: + # A DatasetRefHandle that is the left side of a DotJoinHandle is a valid + # "Dataset.field" reference — only reject truly bare, unqualified dataset refs. + qualified = {id(dj._handle1) for dj in expr_info.dot_join_handles} + for drh in expr_info.dataset_ref_handles: + if id(drh) not in qualified: + raise ValueError( + f"Bare dataset references are not allowed in mapping expressions: " + f"'{drh.name()}' in '{formula_str}'. Use 'DATASET_NAME.field_name' instead." + ) + + @staticmethod + def _validate_no_fact_refs(expr_info: FormulaExpressionInfo, formula_str: str) -> None: + if expr_info.fact_refs: + raise ValueError( + f"Function/apply expressions are not allowed in mapping expressions: '{formula_str}'." + ) \ No newline at end of file diff --git a/converters/ontology/src/ossie_ontology/expr/formula/visitor/__init__.py b/converters/ontology/src/ossie_ontology/expr/formula/visitor/__init__.py new file mode 100644 index 00000000..b984484f --- /dev/null +++ b/converters/ontology/src/ossie_ontology/expr/formula/visitor/__init__.py @@ -0,0 +1,231 @@ +# 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 __future__ import annotations +from typing import Optional, TypeVar, Generic +from abc import abstractmethod + +from ossie_ontology.expr.formula.model import AggExpr, AggregationMethod, Negation, VarHandle, LiteralHandle, \ + DotJoinHandle, FactRef, RelationshipRefHandle, ConceptRefHandle, DatasetRefHandle, DatasetFieldHandle, \ + Conjunction, Disjunction, Exists, WhereExpr +from ossie_ontology.expr.model import Expression, BinOp +from ossie_ontology.expr import Node + + +# -------------------------------------------------- +# Visitor Abstraction for formulas +# -------------------------------------------------- + +Result = TypeVar('Result') + + +class GenericVisitor(Generic[Result]): + + """ + Abstract visitor with handlers for each node type. + Each handler should return a value of type `Result`. + + Actual behavior (e.g., AST traversal) should be implemented in + subclasses of `GenericVisitor`. + """ + + @abstractmethod + def visit_aggexpr(self, node: AggExpr, parent: Optional[Node]) -> Result: + pass + + @abstractmethod + def visit_aggregationmethod(self, node: AggregationMethod, parent: Optional[Node]) -> Result: + pass + + @abstractmethod + def visit_expression(self, node: Expression, parent: Optional[Node]) -> Result: + pass + + @abstractmethod + def visit_binop(self, node: BinOp, parent: Optional[Node]) -> Result: + pass + + @abstractmethod + def visit_negation(self, node: Negation, parent: Optional[Node]) -> Result: + pass + + @abstractmethod + def visit_conjunction(self, node: Conjunction, parent: Optional[Node]) -> Result: + pass + + @abstractmethod + def visit_disjunction(self, node: Disjunction, parent: Optional[Node]) -> Result: + pass + + @abstractmethod + def visit_exists(self, node: Exists, parent: Optional[Node]) -> Result: + pass + + @abstractmethod + def visit_whereexpr(self, node: WhereExpr, parent: Optional[Node]) -> Result: + pass + + @abstractmethod + def visit_varhandle(self, node: VarHandle, parent: Optional[Node]) -> Result: + pass + + @abstractmethod + def visit_literalhandle(self, node: LiteralHandle, parent: Optional[Node]) -> Result: + pass + + @abstractmethod + def visit_conceptrefhandle(self, node: ConceptRefHandle, parent: Optional[Node]) -> Result: + pass + + @abstractmethod + def visit_relationshiprefhandle(self, node: RelationshipRefHandle, parent: Optional[Node]) -> Result: + pass + + @abstractmethod + def visit_datasetrefhandle(self, node: DatasetRefHandle, parent: Optional[Node]) -> Result: + pass + + @abstractmethod + def visit_datasetfieldhandle(self, node: DatasetFieldHandle, parent: Optional[Node]) -> Result: + pass + + @abstractmethod + def visit_factref(self, node: FactRef, parent: Optional[Node]) -> Result: + pass + + @abstractmethod + def visit_dotjoinhandle(self, node: DotJoinHandle, parent: Optional[Node]) -> Result: + pass + + +class Visitor(GenericVisitor[None]): + """ + A visitor that just walks the Expression tree, performing no changes. + """ + + def enter(self, node: Node, parent: Optional[Node] = None) -> Visitor: + """ Visit the node, possibly returning a new visitor to be used to visit the node's children. """ + return self + + def leave(self, node: Node, parent: Optional[Node] = None) -> Node: + """ Visit the node after visiting it and its children. """ + return node + + def _walk_node(self, node: Node, parent: Optional[Node] = None): + # Enter the node, returning a new context. + v = self.enter(node, parent) + + # Dispatch to the handler for the node. + # By default the handler traverses the node's children and reconstructs the node. + node.accept(v, parent) + + self.leave(node, parent) + + def visit_aggexpr(self, node: AggExpr, parent: Optional[Node]): + self._walk_node(node._method, node) + for var in node._var_seq: + self._walk_node(var, node) + if node._formula is not None: + self._walk_node(node._formula, node) + for var in node._group_by: + self._walk_node(var, node) + + def visit_aggregationmethod(self, node: AggregationMethod, parent: Optional[Node]): + pass + + def visit_expression(self, node: Expression, parent: Optional[Node]): + self._walk_node(node._op, node) + for arg in node._args: + self._walk_node(arg, node) + + def visit_binop(self, node: BinOp, parent: Optional[Node]): + pass + + def visit_negation(self, node: Negation, parent: Optional[Node]): + self._walk_node(node._arg, node) + + def visit_varhandle(self, node: VarHandle, parent: Optional[Node]): + pass + + def visit_literalhandle(self, node: LiteralHandle, parent: Optional[Node]): + pass + + def visit_conceptrefhandle(self, node: ConceptRefHandle, parent: Optional[Node]): + pass + + def visit_relationshiprefhandle(self, node: RelationshipRefHandle, parent: Optional[Node]): + pass + + def visit_datasetrefhandle(self, node: DatasetRefHandle, parent: Optional[Node]): + pass + + def visit_datasetfieldhandle(self, node: DatasetFieldHandle, parent: Optional[Node]): + pass + + def visit_factref(self, node: FactRef, parent: Optional[Node]): + self._walk_node(node._handle, node) + for arg in node._args: + self._walk_node(arg, node) + + def visit_dotjoinhandle(self, node: DotJoinHandle, parent: Optional[Node]): + self._walk_node(node._handle1, node) + self._walk_node(node._handle2, node) + + def visit_conjunction(self, node: Conjunction, parent: Optional[Node]): + self._walk_node(node.left(), node) + self._walk_node(node.right(), node) + + def visit_disjunction(self, node: Disjunction, parent: Optional[Node]): + self._walk_node(node.left(), node) + self._walk_node(node.right(), node) + + def visit_exists(self, node: Exists, parent: Optional[Node]): + self._walk_node(node.body(), node) + + def visit_whereexpr(self, node: WhereExpr, parent: Optional[Node]): + self._walk_node(node.expr(), node) + self._walk_node(node.formula(), node) + + +class GenericFormulaConverter(GenericVisitor): + """Base class for formula-to-PyRel converters. Provides shared operator evaluation.""" + + @staticmethod + def _apply_operator(left, op: BinOp, right): + match op: + case BinOp.EQ: + return left == right + case BinOp.NE: + return left != right + case BinOp.LE: + return left <= right + case BinOp.GE: + return left >= right + case BinOp.PLUS: + return left + right + case BinOp.MINUS: + return left - right + case BinOp.TIMES: + return left * right + case BinOp.DIVIDE: + return left / right + case BinOp.LT: + return left < right + case BinOp.GT: + return left > right + case _: + raise ValueError(f"Unknown operator: {op}") diff --git a/converters/ontology/src/ossie_ontology/expr/formula/visitor/collector.py b/converters/ontology/src/ossie_ontology/expr/formula/visitor/collector.py new file mode 100644 index 00000000..8f54e50c --- /dev/null +++ b/converters/ontology/src/ossie_ontology/expr/formula/visitor/collector.py @@ -0,0 +1,139 @@ +# 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 __future__ import annotations +from typing import Optional + +from ossie_ontology.expr.formula.model import ( + VarHandle, + LiteralHandle, + DotJoinHandle, + FactRef, + RelationshipRefHandle, + ConceptRefHandle, + DatasetRefHandle, + DatasetFieldHandle, + FormulaExpressionInfo, + Var, +) +from ossie_ontology.expr.formula.visitor import Visitor +from ossie_ontology.expr.model import Expression, BinOp +from ossie_ontology.expr import Node +from ossie_ontology.model import Concept + + +class FormulaCollector(Visitor): + """FormulaCollector — collects Handles in an Expression + tree while inferring concept types against model's Concept.""" + + def __init__(self, head_vars: dict[Var, Concept]): + self._var_handles: list[tuple[VarHandle, Optional[Concept]]] = [] + # Seeded from the head vars, which are keyed by either handle kind, then + # extended with the body's plain vars as their types are inferred. + self._resolved_var_handles: dict[Var, Optional[Concept]] = dict(head_vars) + self._literal_handles: list[LiteralHandle] = [] + self._concept_ref_handles: list[tuple[ConceptRefHandle, Concept]] = [] + self._relationship_ref_handles: list[RelationshipRefHandle] = [] + self._dataset_ref_handles: list[DatasetRefHandle] = [] + self._dataset_field_handles: list[DatasetFieldHandle] = [] + self._fact_refs: list[FactRef] = [] + self._dot_join_handles: list[DotJoinHandle] = [] + + def expression_info(self) -> FormulaExpressionInfo: + return FormulaExpressionInfo( + var_handles=self._var_handles, + literal_handles=self._literal_handles, + concept_ref_handles=self._concept_ref_handles, + relationship_ref_handles=self._relationship_ref_handles, + dataset_ref_handles=self._dataset_ref_handles, + dataset_field_handles=self._dataset_field_handles, + fact_refs=self._fact_refs, + dot_join_handles=self._dot_join_handles, + ) + + def visit_varhandle(self, node: VarHandle, parent: Optional[Node]): + var_type = self._resolved_var_handles.get(node) + if not var_type: + var_type = self._infer_type(node, parent) + if var_type: + self._resolved_var_handles.setdefault(node, var_type) + self._var_handles.append((node, var_type)) + + def visit_literalhandle(self, node: LiteralHandle, parent: Optional[Node]): + self._literal_handles.append(node) + + def visit_conceptrefhandle(self, node: ConceptRefHandle, parent: Optional[Node]): + self._concept_ref_handles.append((node, node._concept)) + + def visit_relationshiprefhandle(self, node: RelationshipRefHandle, parent: Optional[Node]): + if isinstance(parent, Expression): + self._collect_relationship_crh(node) + self._relationship_ref_handles.append(node) + + def visit_datasetrefhandle(self, node: DatasetRefHandle, parent: Optional[Node]): + self._dataset_ref_handles.append(node) + + def visit_datasetfieldhandle(self, node: DatasetFieldHandle, parent: Optional[Node]): + self._dataset_field_handles.append(node) + + def visit_factref(self, node: FactRef, parent: Optional[Node]): + super().visit_factref(node, parent) + self._fact_refs.append(node) + + def visit_dotjoinhandle(self, node: DotJoinHandle, parent: Optional[Node]): + if isinstance(parent, Expression) and isinstance(node._handle2, RelationshipRefHandle): + self._collect_relationship_crh(node._handle2) + super().visit_dotjoinhandle(node, parent) + self._dot_join_handles.append(node) + + def _collect_relationship_crh(self, rel_handle: RelationshipRefHandle): + for r in rel_handle._relationship._roles: + self._concept_ref_handles.append((ConceptRefHandle(r._player), r._player)) + + def _infer_type(self, node: VarHandle, parent: Optional[Node]) -> Optional[Concept]: + if parent is None: + return None + if isinstance(parent, DotJoinHandle) and isinstance(parent._handle2, RelationshipRefHandle): + if parent._handle1 == node: + return self._resolve_from_first(parent._handle2) + elif isinstance(parent, FactRef): + if isinstance(parent._handle, RelationshipRefHandle): + return self._resolve_from_args(node, parent._handle, parent._args) + if isinstance(parent._handle, DotJoinHandle) and isinstance(parent._handle._handle2, RelationshipRefHandle): + rel_ref = parent._handle._handle2 + args = parent._args if len(parent._args) == rel_ref._relationship.arity else [ + parent._handle._handle1, *parent._args + ] + return self._resolve_from_args(node, rel_ref, args) + elif isinstance(parent, Expression) and parent._op in BinOp.comparison_ops: + left, right = parent._args + target = right if left == node else left + if isinstance(target, DotJoinHandle) and isinstance(target._handle2, RelationshipRefHandle): + return self._resolve_from_last(target._handle2) + return None + + def _resolve_from_args(self, var: VarHandle, rel: RelationshipRefHandle, args) -> Optional[Concept]: + for i, arg in enumerate(args): + if arg == var: + return rel._relationship._roles[i]._player + return None + + def _resolve_from_first(self, node: RelationshipRefHandle) -> Optional[Concept]: + return node._relationship._roles[0]._player + + def _resolve_from_last(self, node: RelationshipRefHandle) -> Optional[Concept]: + return node._relationship._roles[-1]._player \ No newline at end of file diff --git a/converters/ontology/src/ossie_ontology/expr/formula/visitor/converter.py b/converters/ontology/src/ossie_ontology/expr/formula/visitor/converter.py new file mode 100644 index 00000000..3b31e765 --- /dev/null +++ b/converters/ontology/src/ossie_ontology/expr/formula/visitor/converter.py @@ -0,0 +1,394 @@ +# 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. + +"""Formula AST -> PyRel rules. + +The one module under `ossie_ontology.expr` that imports `relationalai`. It sits +here rather than with the converter because it is a `GenericFormulaConverter` +visitor, alongside `collector` and `unbound` — the difference is only what it +emits. Nothing in the `expr` package imports it, so parsing and validating +formulas still works with the SDK absent; it is reached solely from +`ossie_ontology.converter.ossie_to_relationalai`, which the optional +`relationalai` extra installs. +""" + +from __future__ import annotations + +from typing import Optional + +from relationalai.util.naming import NameCache +import relationalai.semantics as pyrel +from relationalai.semantics import not_ +from relationalai.semantics.frontend.base import Ref, Concept as RAIConcept, CoreConcepts, Chain, \ + Relationship as RAIRelationship + +from ossie_ontology.reasoner import OntologyReasoner +from ossie_ontology.vendor.relationalai.mappings import ValueMapping +from ossie_ontology.vendor.relationalai.ontology import OntologyModel +from ossie_ontology.expr.formula.model import ( + AggregationMethod, + Conjunction, + Disjunction, + WhereExpr, + DatasetFieldHandle, + DatasetRefHandle, + DotJoinHandle, + VarHandle, + ConceptRefHandle, + RelationshipRefHandle, + FactRef, + LiteralHandle, + Negation, + AggExpr, + Formula +) +from ossie_ontology.expr.formula.visitor import GenericFormulaConverter +from ossie_ontology.expr import Node +from ossie_ontology.expr.common import AggMethod as ModelAggMethod +from ossie_ontology.expr.model import Expression, BinOp +from ossie_ontology.model import OntologyComponent, Concept, Relationship, Role + +# The Ossie aggregation vocabulary, resolved to the pyrel builtins that implement +# it. This is the only place the two vocabularies meet: `AggMethod` itself is +# vendor-neutral and lives in `ossie_ontology.expr.common` alongside the grammar +# that produces it, so nothing in the Ossie layer has to know pyrel exists. +SEMANTIC_MODEL_TO_PYREL_AGG = { + ModelAggMethod.SUM: pyrel.sum, + ModelAggMethod.MIN: pyrel.min, + ModelAggMethod.MAX: pyrel.max, + ModelAggMethod.AVG: pyrel.avg, + ModelAggMethod.COUNT: pyrel.count, +} + +class FormulaConverter(GenericFormulaConverter): + """FormulaConverter — generates PyRel rules from a Formula.""" + + def __init__( + self, + oc: OntologyComponent, + ontology: OntologyModel, + reasoner: OntologyReasoner, + var_name_cache: NameCache, + formula: Formula, + ): + self._oc = oc + self._ontology = ontology + self._reasoner = reasoner + self._formula = formula + self._target_var = formula.formula_target() + self._var_name_cache = var_name_cache + self._var_refs: dict = {} + self._body_statements: list = [] + var_handles = dict(self._formula.expr_info().var_handles) + for var, c in var_handles.items(): + if c is None: + continue + pyrel_c = self._ontology.lookup_concept(c.name) + assert pyrel_c is not None, f"Concept '{c.name}' not found in ontology" + # Name the ref after the formula's own variable. `ref()` with no + # name falls back to one derived from the concept, so every + # Decimal-typed variable in a formula came out as `number` — + # distinct Vars sharing one name. That is invisible while the model + # only ever gets compiled (Vars are identified by id), but anything + # that renders the metamodel back to source collapses them: the + # `net_revenue == extended_price * (1 - discount)` formula + # decompiled to `Decimal.number == Decimal.number * (1 - Decimal.number)`. + # `_var_name` is NameCache-backed, so these stay unique across formulas. + var_name = self._var_name(var) + self._var_refs[var_name] = pyrel_c.ref(var_name) + self._ontology.base_model().define(self._head()).where(*self._body()) + + def _head(self): + formula_parent = self._formula.parent + if isinstance(formula_parent, Concept): + pyrel_concept = self._ontology.lookup_concept(formula_parent.name) + assert pyrel_concept is not None, f"Concept '{formula_parent.name}' not found in ontology" + if formula_parent.extends: + subc = self._ontology.lookup_concept(formula_parent.extends[0].name) + return pyrel_concept(subc) + else: + identifiers = {} + # `ref_scheme` returns None for a concept with no identifier + # of its own and no supertype to inherit one from — which is + # exactly this branch, since `extends` was falsy above. The old + # guard re-tested `isinstance(..., Concept)`, already true here, + # so the None case reached `for ... in None`. + for idt in (self._reasoner.ref_scheme(formula_parent) or ()): + identifiers[idt.name] = self._role_var(idt.last_role) + return pyrel_concept.new(**identifiers) + else: + if isinstance(formula_parent, Relationship): + relationship = formula_parent + concept = relationship.first_role.player + else: + assert isinstance(formula_parent, tuple) + concept, relationship = formula_parent + assert ( + self._reasoner.in_subtype_closure(concept, relationship.first_role.player) + or relationship.first_role.player is concept + ) + pyrel_concept = self._ontology.lookup_concept(concept.name) + assert pyrel_concept is not None, f"Concept '{concept.name}' not found in ontology" + if self._reasoner.is_identifier_relationship(relationship): + identifiers = {relationship.name: self._role_var(relationship.last_role)} + return pyrel_concept.new(**identifiers) + else: + args = [self._role_var(r) for r in relationship.roles] + return self._pyrel_relationship(relationship, pyrel_concept)(*args) + + def _body(self) -> list: + for e in self._formula.expr(): + result = e.accept(self) + if result is not None: + self._body_statements.append(result) + return self._body_statements + + def visit_varhandle(self, node: VarHandle, parent: Optional[Node]): + return self._var_refs[self._var_name(node)] + + def visit_literalhandle(self, node: LiteralHandle, parent: Optional[Node]): + return self._target_var[0].accept(self) == node._val if self._target_var and not parent else node._val + + def visit_conceptrefhandle(self, node: ConceptRefHandle, parent: Optional[Node]): + return self._ontology.lookup_concept(str(node._concept)) + + def visit_relationshiprefhandle(self, node: RelationshipRefHandle, parent: Optional[Node]): + rel = node._relationship + if self._target_var and parent and isinstance(parent, Expression): + rel_args = [] + for r in rel.roles[:-1]: + rel_args.append(self._role_var(r)) + pyrel_last = self._ontology.lookup_concept(rel.last_role.player.name) + assert pyrel_last is not None + # Named for the same reason as the var refs above: an unnamed ref + # inherits a concept-derived name, so several of these in one + # formula become indistinguishable once rendered back to source. + var_ref = pyrel_last.ref(self._var_name(f"{rel.name}_{rel.last_role.player.name}")) + rel_args.append(var_ref) + self._body_statements.append(self._pyrel_relationship(rel)(*rel_args)) + return var_ref + if rel.container: + pyrel_concept = self._ontology.lookup_concept(rel.container.name) + assert pyrel_concept is not None + return pyrel_concept.__getattr__(rel.name) + else: + return self._ontology.lookup_relationship(rel.name) + + def visit_datasetrefhandle(self, node: DatasetRefHandle, parent: Optional[Node]): + return self._ontology.lookup_table(node.name()) + + def visit_datasetfieldhandle(self, node: DatasetFieldHandle, parent: Optional[Node]): + pass + + def visit_factref(self, node: FactRef, parent: Optional[Node]): + handle = node._handle.accept(self, node) + args = [arg.accept(self, node) for arg in node._args] + return handle(*args) + + def visit_expression(self, node: Expression, parent: Optional[Node]): + left, right = node._args + pyrel_left = left.accept(self, node) + pyrel_right = right.accept(self, node) + pyrel_expr = self._apply_operator(pyrel_left, node._op, pyrel_right) + if (parent is not None and isinstance(parent, Expression) + and parent._op in BinOp.multiplicative_ops + and node._op not in BinOp.multiplicative_ops): + pyrel_expr = (pyrel_expr) + if not self._target_var or parent: + return pyrel_expr + target = self._target_var[0].accept(self) + if target is pyrel_left or target is pyrel_right: + return pyrel_expr + return target == pyrel_expr + + def visit_aggexpr(self, node: AggExpr, parent: Optional[Node]): + pyrel_vars = [var.accept(self, node) for var in node._var_seq] + pyrel_body = self._flatten_formula(node._formula, node) if node._formula is not None else [] + pyrel_per = [var.accept(self, node) for var in node._group_by] + pyrel_agg = SEMANTIC_MODEL_TO_PYREL_AGG[ModelAggMethod(node._method._val)] + pyrel_expr = pyrel_agg(*pyrel_vars) + if pyrel_body: + pyrel_expr = pyrel_expr.where(*pyrel_body) + if pyrel_per: + pyrel_expr = pyrel_expr.per(*pyrel_per) + return self._target_var[0].accept(self) == pyrel_expr if self._target_var and not parent else pyrel_expr + + def visit_conjunction(self, node: Conjunction, parent: Optional[Node]): + left_result = node.left().accept(self, node) + right_result = node.right().accept(self, node) + if left_result is not None: + self._body_statements.append(left_result) + if right_result is not None: + self._body_statements.append(right_result) + + def visit_disjunction(self, node: Disjunction, parent: Optional[Node]): + raise NotImplementedError("Disjunction (OR) in formula body is not yet supported") + + def visit_whereexpr(self, node: WhereExpr, parent: Optional[Node]): + raise NotImplementedError("WHERE expression is not yet supported") + + def visit_negation(self, node: Negation, parent: Optional[Node]): + return not_(node._arg.accept(self, node)) + + def visit_dotjoinhandle(self, node: DotJoinHandle, parent: Optional[Node]): + handle1 = node._handle1.accept(self) + if isinstance(node._handle2, DatasetFieldHandle) and self._target_var and not parent: + return handle1.__getattr__(str(node._handle2))(self._target_var[0].accept(self)) + return handle1.__getattr__(str(node._handle2)) + + def visit_aggregationmethod(self, node: AggregationMethod, parent: Optional[Node]): + pass + + def visit_binop(self, node: BinOp, parent: Optional[Node]): + pass + + def _flatten_formula(self, formula_node, parent) -> list: + if isinstance(formula_node, Conjunction): + return (self._flatten_formula(formula_node.left(), parent) + + self._flatten_formula(formula_node.right(), parent)) + result = formula_node.accept(self, parent) + return [result] if result is not None else [] + + def _role_var(self, role: Role) -> RAIConcept | Ref: + role_name = role.name + var_key = self._var_name(role_name) + if var_key in self._var_refs: + return self._var_refs[var_key] + pyrel_c = self._ontology.lookup_concept(role_name) + assert pyrel_c is not None, f"Concept '{role_name}' not found in ontology" + return pyrel_c + + def _var_name(self, var: str | VarHandle) -> str: + name = var.name() if isinstance(var, VarHandle) else var + return self._var_name_cache.get_name(f"{self._formula.raw_expr}_{name}", name) + + def _pyrel_relationship(self, relationship: Relationship, container: Optional[RAIConcept] = None) -> RAIRelationship | Chain: + if container is None: + concept = self._ontology.lookup_concept(relationship.first_role.player.name) + assert concept is not None, f"Concept '{relationship.first_role.player.name}' not found in ontology" + else: + concept = container + if concept._name in CoreConcepts: + rel = self._ontology.lookup_relationship(relationship.name) + assert rel is not None, f"Relationship '{relationship.name}' not found in ontology" + return rel + else: + return concept.__getattr__(relationship.name) + +class MappingFormulaConverter(GenericFormulaConverter): + """Converts a mapping formula AST to a PyRel expression. + + Tables are resolved dynamically from the DatasetField.dataset back-reference rather than requiring a fixed table + to be passed in. This allows mapping expressions that span multiple datasets. + + The result of the conversion is available via the `result` attribute after construction. + """ + + def __init__(self, ontology: OntologyModel, formula: Formula, concept=None): + self._ontology = ontology + self._concept = concept + expr = formula.expr()[0] + if isinstance(expr, WhereExpr): + self.result = expr.accept(self, None) + else: + raw = expr.accept(self, None) + if self._concept is not None: + raw = self._concept(raw) + self.result = ValueMapping(raw) + + def visit_datasetfieldhandle(self, node: DatasetFieldHandle, parent): + field = node._field + dataset = getattr(field, 'dataset', None) + if dataset is not None: + table = self._ontology.lookup_table(dataset.name()) + assert table is not None, f"Table '{dataset.name()}' not found in ontology" + return table.__getattr__(field.name) + raise ValueError(f"Cannot resolve table for field '{field.name}' — dataset back-reference is missing") + + def visit_dotjoinhandle(self, node: DotJoinHandle, parent): + if isinstance(node._handle2, DatasetFieldHandle): + field = node._handle2._field + dataset = getattr(field, 'dataset', None) + if dataset is not None: + table = self._ontology.lookup_table(dataset.name) + assert table is not None, f"Table '{dataset.name}' not found in ontology" + return table.__getattr__(field.name) + if isinstance(node._handle1, DatasetRefHandle): + table = self._ontology.lookup_table(node._handle1.name()) + assert table is not None, f"Table '{node._handle1.name()}' not found in ontology" + return table.__getattr__(field.name) + raise ValueError(f"Unsupported dot-join in mapping expression: '{node}'") + + def visit_literalhandle(self, node: LiteralHandle, parent): + return node._val + + def visit_expression(self, node: Expression, parent): + left = node._args[0].accept(self, node) + right = node._args[1].accept(self, node) + return self._apply_operator(left, node._op, right) + + def visit_aggexpr(self, node: AggExpr, parent): + pyrel_vars = [v.accept(self, node) for v in node._var_seq] + pyrel_agg_fn = SEMANTIC_MODEL_TO_PYREL_AGG[ModelAggMethod(node._method._val)] + pyrel_expr = pyrel_agg_fn(*pyrel_vars) + if node._formula is not None: + pyrel_expr = pyrel_expr.where(*self._flatten_conditions(node._formula)) + if node._group_by: + pyrel_expr = pyrel_expr.per(*[g.accept(self, node) for g in node._group_by]) + return pyrel_expr + + def visit_varhandle(self, node: VarHandle, parent): + raise ValueError(f"Variable references are not allowed in mapping expressions: '{node}'") + + def visit_conceptrefhandle(self, node: ConceptRefHandle, parent): + raise ValueError(f"Concept references are not allowed in mapping expressions: '{node._concept}'") + + def visit_relationshiprefhandle(self, node: RelationshipRefHandle, parent): + raise ValueError(f"Relationship references are not allowed in mapping expressions: '{node._relationship}'") + + def visit_datasetrefhandle(self, node: DatasetRefHandle, parent): + raise ValueError(f"Bare dataset references are not allowed in mapping expressions: '{node.name()}'") + + def visit_factref(self, node: FactRef, parent): + raise ValueError("Function/apply expressions are not allowed in mapping expressions.") + + def visit_negation(self, node: Negation, parent): + return not_(node._arg.accept(self, parent)) + + def visit_conjunction(self, node: Conjunction, parent): + raise ValueError("Conjunction (AND) is only valid inside an aggregation WHERE condition in mapping expressions.") + + def visit_disjunction(self, node: Disjunction, parent): + raise ValueError("Disjunction (OR) is not allowed in mapping expressions.") + + def visit_whereexpr(self, node: WhereExpr, parent): + left = node.expr().accept(self, node) + right = node.formula().accept(self, node) + if self._concept is not None: + left = self._concept(left) + return ValueMapping(left).where(right) + + def visit_aggregationmethod(self, node: AggregationMethod, parent): + pass + + def visit_binop(self, node: BinOp, parent): + pass + + def _flatten_conditions(self, node) -> list: + if isinstance(node, Conjunction): + return self._flatten_conditions(node._left) + self._flatten_conditions(node._right) + return [node.accept(self, None)] \ No newline at end of file diff --git a/converters/ontology/src/ossie_ontology/expr/formula/visitor/unbound.py b/converters/ontology/src/ossie_ontology/expr/formula/visitor/unbound.py new file mode 100644 index 00000000..6741dc5f --- /dev/null +++ b/converters/ontology/src/ossie_ontology/expr/formula/visitor/unbound.py @@ -0,0 +1,220 @@ +# 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. + +"""Unbound-reference analysis for a parsed formula. + +A formula may only name references that something binds. What binds them is the +formula's *scope* — its head variables and its parent concept — and the joins the +formula itself writes: + + concept Person, requires: + 'Person.employer(Company) AND Company.revenue > 0' ok, the fact joins them + 'Person.name AND Company.revenue > 0' 'Company' is unbound + 'Person.name AND EXISTS(Company.revenue > 0)' ok, EXISTS binds it + +An ontology-level `requires` has no parent, so its scope is empty and every +reference has to be bound by the formula itself. + +This is a read-only pass over the AST that `FormulaValidator` runs; it is kept +apart from `FormulaCollector`, which is about collecting handles and inferring +their types. +""" +from __future__ import annotations + +from typing import Optional + +from ossie_ontology.expr.formula.model import ( + AggExpr, + ConceptRefHandle, + DotJoinHandle, + Exists, + FactRef, + Formula, + VarHandle, +) +from ossie_ontology.expr.formula.visitor import Visitor +from ossie_ontology.expr.model import BinOp, Expression +from ossie_ontology.expr import Node +from ossie_ontology.model import Concept, FormulaParent, Relationship + +Ref = VarHandle | ConceptRefHandle + + +class UnboundRefFinder(Visitor): + """Finds the references a formula names but never binds. + + Construct it with the formula and ask it for `unbound_refs()`. The walk + collects three things, which that method then combines: + + * the references that appear outside any binder — only these have to be + bound, since a binder scopes its own (see `visit_exists`); + * the references each fact atom joins, because traversing a fact binds its + participants to one another; + * the references an equality defines a variable from. + + Scope then spreads from the head outwards along the joins and definitions, + and whatever it never reaches is unbound. + """ + + def __init__(self, formula: Formula): + self._formula = formula + self._scope = formula_scope(formula) + self._refs: list[Ref] = [] + self._joins: list[list[Ref]] = [] + self._definitions: list[tuple[VarHandle, list[Ref]]] = [] + self._in_binder = False + + def unbound_refs(self) -> list[Ref]: + """The references the formula names but never binds, first-seen first.""" + self._refs, self._joins, self._definitions = [], [], [] + for expr in self._formula.expr(): + expr.accept(self) + return self._unreached_by_scope() + + # ---------------- collecting ------------------------------------------ + + def visit_varhandle(self, node: VarHandle, parent: Optional[Node]): + # The step after a dot names a relationship, not a variable. It only + # shows up as a VarHandle when the left side's type is unknown until + # inference, as with the 'sub.staff' of + # 'Company.subsidiary(sub) AND sub.staff > 0' — `_validate_dot_join` + # resolves it against the relationship, so it is never a free variable. + if isinstance(parent, DotJoinHandle) and parent._handle2 is node: + return + self._collect_ref(node) + + def visit_conceptrefhandle(self, node: ConceptRefHandle, parent: Optional[Node]): + self._collect_ref(node) + + def visit_aggexpr(self, node: AggExpr, parent: Optional[Node]): + # 'Person.name AND COUNT[Company] > 0' — the aggregate binds `Company`, + # so nothing under it is collected. Saving the outer value instead of + # counting is what handles nesting: an inner binder restores `True` on + # its way out, and only the outermost one restores `False`. + outer, self._in_binder = self._in_binder, True + super().visit_aggexpr(node, parent) + self._in_binder = outer + + def visit_exists(self, node: Exists, parent: Optional[Node]): + # Same for EXISTS, at any nesting depth and even when the EXISTS is the + # whole formula, as in an ontology-level 'EXISTS(Person.ssn > 0)'. + outer, self._in_binder = self._in_binder, True + super().visit_exists(node, parent) + self._in_binder = outer + + def visit_factref(self, node: FactRef, parent: Optional[Node]): + super().visit_factref(node, parent) + if self._in_binder: + return + # Traversing a fact binds its participants to each other, so + # 'Person.employer(Company)' joins `Person` and `Company`: once either + # is in scope, so is the other. + joined = [h for h in (_anchor(node._handle), *node._args) if isinstance(h, (VarHandle, ConceptRefHandle))] + if len(joined) > 1: + self._joins.append(joined) + + def visit_expression(self, node: Expression, parent: Optional[Node]): + # Walks the arguments here rather than in the base visitor so that the + # references each side contributes can be sliced off `_refs`: an + # equality defines its variable in terms of the other side, as with the + # 'extended_price == OrderLineItem.lineitem_extended_price' that puts + # `extended_price` in scope for the rest of the formula. + self._walk_node(node._op, node) + per_arg: list[list[Ref]] = [] + for arg in node._args: + seen = len(self._refs) + self._walk_node(arg, node) + per_arg.append(self._refs[seen:]) + if self._in_binder or node._op != BinOp.EQ or len(node._args) != 2: + return + for defined, defining in ((0, 1), (1, 0)): + handle = node._args[defined] + if isinstance(handle, VarHandle): + self._definitions.append((handle, per_arg[defining])) + + def _collect_ref(self, node: Ref): + if not self._in_binder: + self._refs.append(node) + + # ---------------- deciding -------------------------------------------- + + def _unreached_by_scope(self) -> list[Ref]: + """The collected references that scope does not reach, first-seen first.""" + bound = set(self._scope) + # A fact binds all of its references as soon as one of them is bound; an + # equality binds its variable once the side defining it is fully bound. + # Iterate until nothing new is reached, since the formula may join the + # scope in any order — 'A.r(B) AND C.s(A)' reaches `B` before `C`. + expanding = True + while expanding: + expanding = False + for joined in self._joins: + if any(h in bound for h in joined): + fresh = [h for h in joined if h not in bound] + bound.update(fresh) + expanding = expanding or bool(fresh) + for defined, defining in self._definitions: + if defined not in bound and all(h in bound for h in defining): + bound.add(defined) + expanding = True + + unbound: list[Ref] = [] + for ref in self._refs: + if ref not in bound and ref not in unbound: + unbound.append(ref) + return unbound + + +def formula_scope(formula: Formula) -> set[Ref]: + """What `formula` may reference for free: its head vars plus the concepts of + its parent. Empty for an ontology-level `requires`, which has no parent.""" + scope: set[Ref] = set(formula.head_vars()) + scope.update(ConceptRefHandle(c) for c in _scope_concepts(formula.parent)) + return scope + + +def _scope_concepts(parent: FormulaParent) -> set[Concept]: + """The parent concept, or every role player of the parent relationship (its + first role is the containing concept), together with their supertypes — a + subtype carves itself out of a supertype, so 'Payment.type == 'card'' is in + scope for `Card extends Payment`.""" + roots: list[Concept] = [] + if isinstance(parent, Concept): + roots.append(parent) + elif isinstance(parent, Relationship): + roots.extend(role.player for role in parent.roles) + elif isinstance(parent, tuple): + concept, rel = parent + roots.append(concept) + roots.extend(role.player for role in rel.roles) + + concepts: set[Concept] = set() + while roots: + concept = roots.pop() + if concept in concepts: + continue + concepts.add(concept) + roots.extend(concept.extends) + return concepts + + +def _anchor(handle: Node) -> Node: + """The leftmost atom of a handle chain — the reference a traversal such as + 'Person.employer' or 'Person.employment(Company).level' starts from.""" + while isinstance(handle, (DotJoinHandle, FactRef)): + handle = handle._handle1 if isinstance(handle, DotJoinHandle) else handle._handle + return handle \ No newline at end of file diff --git a/converters/ontology/src/ossie_ontology/expr/model.py b/converters/ontology/src/ossie_ontology/expr/model.py new file mode 100644 index 00000000..d531abbb --- /dev/null +++ b/converters/ontology/src/ossie_ontology/expr/model.py @@ -0,0 +1,77 @@ +# 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 __future__ import annotations + +from ossie_ontology.expr import acceptor, Node +from enum import Enum + + +@acceptor +class BinOp(str, Node, Enum): + # comparison operators + EQ = "==" + NE = "!=" + LT = "<" + LE = "<=" + GT = ">" + GE = ">=" + comparison_ops = {EQ, NE, LE, LE, GT, GE} + + # arithmetic operators + PLUS = "+" + MINUS = "-" + TIMES = "*" + DIVIDE = "/" + multiplicative_ops = {TIMES, DIVIDE} + arithmetic_ops = {PLUS, MINUS, TIMES, DIVIDE} + + @classmethod + def from_value(cls, value: str) -> BinOp: + """ + Look up an enum instance by a string value (case-insensitive). + """ + if not isinstance(value, str): + raise TypeError("value must be a string") + try: + return cls(value.strip().upper()) + except ValueError as e: + raise ValueError(f"Unknown comparison operator: {value}") from e + + +@acceptor +class Expression(Node): + def __init__(self, op: BinOp, *args): + self._op = op + self._args = args + + def args(self): + return self._args + + def op(self): + return self._op + + def __hash__(self): + return hash((self._op, *([a.__hash__() for a in self._args]))) + + def __str__(self): + left, right = self._args + return f"{left} {self._op.value} {right}" + + def __repr__(self): + args = ", ".join([a.__repr__() for a in self._args]) + return f"Expression{{({self._op.value} {args})}}" diff --git a/converters/ontology/src/ossie_ontology/model.py b/converters/ontology/src/ossie_ontology/model.py index 2f9d1e72..217529ce 100644 --- a/converters/ontology/src/ossie_ontology/model.py +++ b/converters/ontology/src/ossie_ontology/model.py @@ -164,11 +164,15 @@ def is_entity_type(self) -> bool: @property def is_primitive(self) -> bool: - if self.is_builtin: - return True - if self._extends and len(self._extends) == 1: - return self._extends[0].is_primitive - return False + concept = self + seen: set[Concept] = set() + while True: + if concept.is_builtin: + return True + if len(concept._extends) != 1 or concept in seen: + return False + seen.add(concept) + concept = concept._extends[0] @property def is_derived(self) -> bool: diff --git a/converters/ontology/src/ossie_ontology/parser/__init__.py b/converters/ontology/src/ossie_ontology/parser/__init__.py index 049d7a35..86ece1a6 100644 --- a/converters/ontology/src/ossie_ontology/parser/__init__.py +++ b/converters/ontology/src/ossie_ontology/parser/__init__.py @@ -26,6 +26,7 @@ import yaml from ossie_ontology.converter.spec_to_ossie.converter import SpecToOssieConverter +from ossie_ontology.expr.factory import FormulaParserFactory, MappingFormulaParserFactory from ossie_ontology.model import OssieOntology, FormulaFactory, MappingFormulaFactory from ossie_ontology.spec import OssieSpec @@ -41,8 +42,11 @@ def __init__(self, debug: bool = False, self._debug = debug self._model = None self._spec = None - self._formula_factory = formula_factory or FormulaFactory() - self._mapping_formula_factory = mapping_formula_factory or MappingFormulaFactory() + # Parse formulas by default: an unparsed one reaches the model as raw + # text, is skipped by every downstream converter, and vanishes silently. + # Pass the plain `FormulaFactory` to opt out. + self._formula_factory = formula_factory or FormulaParserFactory() + self._mapping_formula_factory = mapping_formula_factory or MappingFormulaParserFactory() def parse(self, path: Path) -> OssieOntology: # Ossie always expects a single spec file. diff --git a/converters/ontology/src/ossie_ontology/reasoner.py b/converters/ontology/src/ossie_ontology/reasoner.py new file mode 100644 index 00000000..a5390a8b --- /dev/null +++ b/converters/ontology/src/ossie_ontology/reasoner.py @@ -0,0 +1,159 @@ +# 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 __future__ import annotations + +from ossie_ontology.model import ( + Concept, + Formula, + OntologyObserver, + Relationship, + RelationshipMultiplicity, + Role, +) + + +class OntologyReasoner(OntologyObserver): + """Answers the questions the converters and the formula layer ask about an + ontology's shape: which relationships construct an entity, and how concepts + sit relative to each other in the type hierarchy. + + Only identifiers are indexed, from concepts as they arrive. Every other + question is answered off the model on demand, so those answers hold + whatever order concepts were registered in. + + Two preconditions, both met by every converter in this package: + + *Register on a finished ontology.* `register()` replays the concepts already + in the component, so registering last costs nothing. Registering early does + cost something: a concept's `identify_by` is resolved in a later pass than + the `add_concept` that announces it -- relationships have to exist first -- + so a reasoner attached mid-build records no identifiers and + `is_identifier_relationship` then answers False for every relationship. + That query is the one thing here that cannot be answered on demand: it asks + whether *any* concept identifies itself by a given relationship, and a + relationship named in a subtype's `identify_by` may be owned by a supertype + that identifies by nothing -- a reverse lookup no forward walk can resolve. + + *`extends` should be acyclic.* `spec_to_ossie` rejects cycles and + `palantir_to_ossie` breaks them, so no converter here can hand one over. + A cycle reaching this class anyway -- from a model assembled by hand -- is + answered rather than fatal: every walk over `extends` stops at an edge that + leads nowhere new. The answers under a cycle are only as meaningful as the + hierarchy that produced them. + """ + + def __init__(self): + # The identifying side of each relationship some concept names in its + # `identify_by` -- the reasoner's only state. + self._constructor_roles: set[Role] = set() + + # --- observer protocol ------------------------------------------------ + + def on_concept_added(self, concept: Concept) -> None: + self.new_concept(concept) + + def on_require_added(self, require: Formula) -> None: + # Not indexed yet. Which constraint a require states — a mandatory + # role, a uniqueness, a cardinality — only comes out of walking the AST + # that `ossie_ontology.expr` attaches. + pass + + # --- ingestion -------------------------------------------------------- + + def new_concept(self, concept: Concept) -> None: + if not concept.is_primitive: + self._register_declared_identifier(concept) + + def _register_declared_identifier(self, concept: Concept) -> None: + """Record the roles a concept's `identify_by` constructs it from.""" + for identifier in concept.identify_by.values(): + self._constructor_roles.add(identifier.last_role) + + # --- queries ---------------------------------------------------------- + + @staticmethod + def ref_scheme(concept: Concept, shallow: bool = False) -> tuple[Relationship, ...] | None: + """The relationships that identify `concept`, nearest inherited scheme + first, or None when neither it nor its supertypes declare one.""" + return OntologyReasoner._ref_scheme(concept, shallow, set()) + + @staticmethod + def _ref_scheme(concept: Concept, shallow: bool, + path: set[Concept]) -> tuple[Relationship, ...] | None: + # `path` holds the concepts on the branch being walked right now, so a + # branch that re-enters a concept it is already inside stops instead of + # looping forever, and the concept is dropped again on the way out. + # A global visited set would answer the same on every acyclic shape -- + # a branch reaching a scheme-bearing ancestor always returns one, so + # nothing it marked could have mattered to a later branch -- but that + # is an argument about the `break` below, and a path set does not need + # it to be true. + if concept in path: + return None + path.add(concept) + try: + ref_schema: list[Relationship] = [] + if not shallow: + for parent in concept.extends: + parent_schema = OntologyReasoner._ref_scheme(parent, False, path) + if parent_schema: + ref_schema.extend(parent_schema) + break + if concept.identify_by: + ref_schema.extend(concept.identify_by.values()) + return tuple(ref_schema) if ref_schema else None + finally: + path.discard(concept) + + def is_constructing_role(self, role: Role) -> bool: + return role in self._constructor_roles + + def is_identifier_relationship(self, relationship: Relationship) -> bool: + if not relationship.binary: + return False + return any(self.is_constructing_role(r) for r in relationship.roles) + + def in_subtype_closure(self, sub: Concept, sup: Concept) -> bool: + """Is `sub` a strict subtype of `sup`, transitively? + + Walked on demand rather than indexed as concepts arrive: an index built + incrementally only sees a parent's own supertypes if that parent was + registered first, so a child-before-parent ordering silently lost the + transitive edges. `extends` chains are short and this is asked a handful + of times per conversion, so the walk costs nothing worth indexing for. + """ + # Iterative, with a visited set rather than the path set `_ref_scheme` + # uses: this answers reachability, and reaching a concept a second time + # cannot change whether `sup` lies beyond it. That also means no + # recursion depth to run out of on a long `extends` chain. + seen: set[Concept] = set() + stack = list(sub.extends) + while stack: + parent = stack.pop() + if parent is sup: + return True + if parent in seen: + continue + seen.add(parent) + stack.extend(parent.extends) + return False + + def is_property(self, rel: Relationship) -> bool: + if rel.arity < 2: + return False + return rel.multiplicity in (RelationshipMultiplicity.MANY_TO_ONE, RelationshipMultiplicity.ONE_TO_ONE) diff --git a/converters/ontology/src/ossie_ontology/vendor/__init__.py b/converters/ontology/src/ossie_ontology/vendor/__init__.py new file mode 100644 index 00000000..39e529da --- /dev/null +++ b/converters/ontology/src/ossie_ontology/vendor/__init__.py @@ -0,0 +1,26 @@ +# 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. + +"""Vendor-side models: how each spoke format represents an ontology. + +Ossie is the hub — `ossie_ontology.model` and `ossie_ontology.spec`. Each +subpackage here is one spoke's own representation, in whichever direction the +converter runs: `palantir` is a format read from, `relationalai` one written to. + +Converters under `ossie_ontology.converter` translate between a vendor model and +the hub, and hold only that translation. +""" diff --git a/converters/ontology/src/ossie_ontology/external/__init__.py b/converters/ontology/src/ossie_ontology/vendor/palantir/__init__.py similarity index 100% rename from converters/ontology/src/ossie_ontology/external/__init__.py rename to converters/ontology/src/ossie_ontology/vendor/palantir/__init__.py diff --git a/converters/ontology/src/ossie_ontology/external/palantir/model.py b/converters/ontology/src/ossie_ontology/vendor/palantir/model.py similarity index 100% rename from converters/ontology/src/ossie_ontology/external/palantir/model.py rename to converters/ontology/src/ossie_ontology/vendor/palantir/model.py diff --git a/converters/ontology/src/ossie_ontology/external/palantir/parser/__init__.py b/converters/ontology/src/ossie_ontology/vendor/palantir/parser/__init__.py similarity index 99% rename from converters/ontology/src/ossie_ontology/external/palantir/parser/__init__.py rename to converters/ontology/src/ossie_ontology/vendor/palantir/parser/__init__.py index c0977f14..8ef9bb5f 100644 --- a/converters/ontology/src/ossie_ontology/external/palantir/parser/__init__.py +++ b/converters/ontology/src/ossie_ontology/vendor/palantir/parser/__init__.py @@ -23,7 +23,7 @@ from typing import Any, Iterable from ossie_ontology.common.utils import camel_to_snake -from ossie_ontology.external.palantir.model import DataSet, DataSetColumn, DataSetModel, ObjectType, Ontology, DataType, \ +from ossie_ontology.vendor.palantir.model import DataSet, DataSetColumn, DataSetModel, ObjectType, Ontology, DataType, \ ArrayDataType, Property, Status, ManyToOneRelation, Relation, ManyToManyRelation, IntermediaryRelation, DataSource from ossie_ontology.common.file_utils import iter_json_files_from_dir_in_zip, open_top_level_file_from_zip, \ iter_json_files_from_dir, get_top_level_json_file_from_dir, validate_dir diff --git a/converters/ontology/src/ossie_ontology/external/palantir/__init__.py b/converters/ontology/src/ossie_ontology/vendor/relationalai/__init__.py similarity index 100% rename from converters/ontology/src/ossie_ontology/external/palantir/__init__.py rename to converters/ontology/src/ossie_ontology/vendor/relationalai/__init__.py diff --git a/converters/ontology/src/ossie_ontology/vendor/relationalai/mappings.py b/converters/ontology/src/ossie_ontology/vendor/relationalai/mappings.py new file mode 100644 index 00000000..15058ad6 --- /dev/null +++ b/converters/ontology/src/ossie_ontology/vendor/relationalai/mappings.py @@ -0,0 +1,385 @@ +# 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 relationalai.semantics import Concept as RAIConcept, Chain +from relationalai.semantics import Relationship as RAIRelationship +from relationalai.semantics.frontend.base import Ref + +# Given some entity type E. An E *ref* is a Python variable that can be used in +# a select query or fragment to range over E objects in the graph. For instance, +# given: +# +# Person = Concept('Person') +# +# the Person object that is declared here is a Person ref, as is Person.ref(). +# Because PyRel conflates types with variables, we use refs to mean the type +# or the variable depending on context. + + +# Given some entity type E. An E *identifier* is an object (Python dictionary) whose: +# +# - keys are the short names of identifying properties (the ref scheme) of E, and +# - values take on one of the following two forms depending on whether the "one role" +# of the property named by the key is played by: +# +# (1) a value type, in which case the value is a PyRel expression that typically +# involves a reference to one or more dataset fields (columns), or +# (2) some entity type E', in which case, the value is an EntityMapping that +# encapsulates details for looking up references of E'. +# +# Identifiers implement "identifier objects" as they are declared using the *identifier* +# key in an entity or general relationship mapping as proposed in the OSI specification +# for ontologies document. + + +# Given some entity type E, this abstract class encapsulates the E ref to use when +# referencing and/or deriving the population of E in the graph, and all details needed +# to generate the PyRel that makes the E ref usable for schema mapping. The specific +# details include: +# +# - The E ref itself (self.ref()) +# - The identifier that will be used to look up E ref objects in PyRel (self.identifier()) +# - The PyRel fragment that applies the identifier to look up E ref objects(self.lookup()) +# - Any constraints that might need to appear in a where(...) clause in order for the +# use of the E ref to be correct (self.constraints()) +# +class Eref: + + # Erefs are parameterized by: + # + # - the entity type (e) being referenced; + # - the creation context, in which this Eref will be used. Set this flag + # if the context creates new instances of e because it allows us to optimize + # the generated PyRel when looking up instances (using e.to_identity rather + # than e.filter_by). + # - a user supplied E identifier, which may contain nested EntityMaps + # + def __init__(self, e: RAIConcept, creation_context=False, **identifier): + self._entity_type = e + self._creation_context = creation_context + (self._identifier_obj, self._constraints) = self._normalize_identifier(**identifier) + + # The identifier object (dictionary) that is passed in when we construct an + # Eref maps identifying property keys to either PyRel expressions or nested + # EntityMappings. But identifier objects must map these keys to PyRel expressions + # exclusively (no nested entity mappings). + # + # This method generates: + # - a proper identifier object (a copy of identifier in which every key that mapped + # to an EntityMapping is replaced with a key that maps to a reference to the entity + # type represented by that mapping, and + # - an array of constraints imposed by any nested EntityMapping in identifier + # + def _normalize_identifier(self, **identifier): + args = {} + constraints = [] + + for k in identifier: + if isinstance(identifier[k], EntityMapping): + args[k] = identifier[k].ref() + constraints += identifier[k].constraints() + elif isinstance(identifier[k], ValueMapping): + args[k] = identifier[k].expr() + constraints += identifier[k].constraints() + else: + args[k] = identifier[k] + + return (args, constraints) + + # Constraints on the variables used to populate the ref. These will appear + # in the where(...) clause of a rule. Will include any constraints that are + # associated with nested entity mappings that are handled when we replace + # those nested entity mappings to create self.identifier(). + # + def constraints(self): + return self._constraints + + # The entity type being referenced + def entity_type(self): + return self._entity_type + + # The identifier object (dictionary) used when referencing instances of + # this entity type. The object maps identifying properties to PyRel expressions + def identifier(self): + return self._identifier_obj + + # Generates a PyRel fragment to look up/constrain the objects in the graph that + # self.ref() will range over when the fragment is used in a rule or query. + # + # The creation_context lets us optimize how we look up these objects. + # + def lookup(self): + ref = self.ref() + if self._creation_context: + # `to_identity` exists on a Concept but not on a Ref, so a creation + # context requires the concept itself. That holds today: the only + # subclass that swaps in a plain Ref is EntityMapping, which never + # creates. Asserting it keeps the coupling visible if that changes. + assert isinstance(ref, RAIConcept), "a creation context needs a concept ref" + return ref.to_identity(**(self.identifier()), unsafe=True) + return ref.filter_by(**(self.identifier())) + + # The reference variable (of type self._entity_type) to use in generated + # code. This ref is bound by the self.lookup() atom. This method is + # overridden by some subclasses. + # + # A subclass may hand back a plain `Ref` instead of the concept — see + # EntityMapping below — so the declared type covers both. + def ref(self) -> RAIConcept | Ref: + return self._entity_type + + +# Given some entity type E, this class implements Erefs that range over E objects +# without populating any concepts or relationships as a side effect. These are used +# strictly to aid in referencing E objects in larger mapping contexts as we do when +# we declare nested entity mappings in our OSI proposal (mappings section) + +class EntityMapping(Eref): + + def __init__(self, e: RAIConcept, **identifier): + super().__init__(e, **identifier) + self._ref = e.ref() + + def constraints(self): + wc = super().constraints() + wc.append(self.lookup()) + return wc + + def ref(self): + return self._ref + + # Additional conditions (PyRel fragments) that constrain this EntityMapping. + # These will typically refer to Dataset fields + def where(self, expr): + self._constraints.append(expr) + return self + + +class ValueMapping: + def __init__(self, expr): + self._constraints = [] + self._expr = expr + + def constraints(self): + return self._constraints + + def expr(self): + return self._expr + + # Additional conditions (PyRel fragments) that constrain this ValueMapping. + # These will typically refer to Dataset fields + def where(self, expr): + self._constraints.append(expr) + return self + + +class EBinding(Eref): + + # If provided, stype must be a RAIConcept that provides (at least part of) + # the identifier + def __init__(self, model, e: RAIConcept, stype: RAIConcept|None, **identifier): + + if model.declares_preferred_identifier(e): + # e provides a ref scheme + super().__init__(e, creation_context=True, **identifier) + self._model = model + + where_clauses = self._constraints + if len(where_clauses) == 0: + self._model.define(e.new(**(self.identifier()))) + else: + self._model.define(e.new(**(self.identifier()))).where(*where_clauses) + + else: + # e must inherit its ref scheme + if e._name in model._super_type_of: + super().__init__(e, **identifier) + self._model = model + + if stype is not None: + super_ref = EntityMapping(stype, **identifier) + # The rule that "asserts" the population of self.entity_type() + self._model.where(*(super_ref.constraints())).define(e(super_ref.ref())) + # else: Assume the supertype binding is declared elsewhere (e.g., when e is a derived subtype) + + else: + raise ValueError(f'Currently no way to bind entity concept {e._name} because it neither declares nor ' + 'inherits an identifier') + + # + # Declares that a property of this entity type binds to one or more role + # values that ultimately refer to fields of this table. Each value can be: + # + # 1) An arbitrary PyRel expression that references one or more + # fields to produce some value; or + # 2) An EntityMapping that declares how to look up an instance of + # some entity type to populate for each row of the table; or + # 3) A ValueMapping that carries a PyRel expression together with + # WHERE constraints (e.g. produced by a WHERE-filtered mapping formula). + # + # Binary relationships take a single expr. Ternary (and higher-arity) + # relationships pass the intermediate role value(s) via extra_exprs — + # e.g. binds_to(returns, item_ref, amount_expr) for Transaction.returns(Item, Amount). + # Passing no expr at all asserts a unary (zero-role) relationship fact. + # + def binds_to(self, property, *exprs): + ref = self.lookup() + where_clauses = [ref] + where_clauses += self.constraints() + + # Unwrap each role value: extract the PyRel expression and accumulate + # any WHERE constraints the value carries (EntityMapping / ValueMapping). + def _resolve(e): + if isinstance(e, EntityMapping): + where_clauses.extend(e.constraints()) + return e.ref() + if isinstance(e, ValueMapping): + where_clauses.extend(e.constraints()) + return e.expr() + return e + + vals = [_resolve(e) for e in exprs if e is not None] + + atom = EBinding._property_atom(ref, property, *vals) + self._model.where(*where_clauses).define(atom) + + return self + + # + # Helper function that declares automates the construction of an EntityMapping + # by analyzing the property to determine the concept that plays the target role. + # The identifier contains the arguments to that EntityMapping + # + def binds_to_entity(self, property, **identifier): + emap = EntityMapping(self._model._target_role_player(property), **identifier) + return self.binds_to(property, emap) + + # + # Builds the PyRel atom that populates a property of ref with the given + # role values. Accepts zero or more values: + # - zero vals: asserts a unary (boolean) relationship fact + # - one val: binary relationship (the common case) + # - N vals: ternary / higher-arity relationship + # + @staticmethod + def _property_atom(ref, property, *vals): + attr = ref.__getattr__(property._short_name) + return attr(*vals) if vals else attr + + +class JoinPathBinding: + + def __init__(self, model, r: RAIRelationship): + self._model = model + self._relationship = r + self._src_concept = model._source_role_player(r) + self._trg_concept = model._target_role_player(r) + self._bind_from = None + self._bind_to = None + + # Every JoinPathBinding populates the relationship using entity objects that + # it locates using entity mappings (emap1 and emap2) + # + def bind(self, emap1, emap2): + where_clauses = emap1.constraints() + emap2.constraints() + self._model.where(*where_clauses).define(self._relationship(emap1.ref(), emap2.ref())) + return self + + # + # Helper function that creates automate construction of EntityMappings + # by analyzing the concepts that play the from and to roles of the + # relationship being bound + # + + def bind_from(self, **ctor_args): + from_emap = EntityMapping(self._src_concept, **ctor_args) + if self._bind_to is None: + self._bind_from = from_emap + else: + return self.bind(from_emap, self._bind_to) + return self + + def bind_to(self, **ctor_args): + to_emap = EntityMapping(self._trg_concept, **ctor_args) + if self._bind_from is None: + self._bind_to = to_emap + else: + return self.bind(self._bind_from, to_emap) + return self + + +class GRBinding: + + # We can initialize a generalized relationship binding with an array + # of exprs, any one of which can be either: + # + # 1) An arbitrary PyRel expression that references one or more + # fields to produce some value; or + # 2) An EntityMapping that declares how to look up an instance of + # some entity type to populate for each row of the table + # + def __init__(self, model, *exprs): + self._model = model + self._key_constraints = [] + self._keys = [] + + for expr in exprs: + if isinstance(expr, EntityMapping): + self._key_constraints += expr.constraints() + self._keys.append(expr.ref()) + else: + self._keys.append(expr) + + def keys(self): + return self._keys + + def bind(self, handle, expr=None, **identifier): + r = handle._next if isinstance(handle, Chain) else handle + keys = self._keys.copy() + where_clauses = self._key_constraints.copy() + + has_extra = expr is not None or len(identifier) > 0 + self._validate_role_count(r, len(keys) + (1 if has_extra else 0)) + + if has_extra: + extra_key, extra_constraints = self._resolve_extra_key(expr, identifier) + keys.append(extra_key) + where_clauses += extra_constraints + + self._model.define(handle(*keys)).where(*where_clauses) + return self + + @staticmethod + def _validate_role_count(r, expected): + if len(r._fields) != expected: + raise ValueError( + f"Relationship {r} has {len(r._fields)} roles but used in a " + f"GRBinding context that expects {expected}") + + def _resolve_extra_key(self, expr, identifier): + """Resolve the trailing key/constraints pair from either an explicit + expr (EntityMapping / ValueMapping / raw PyRel expression) or + **identifier kwargs that synthesize an EntityMapping for the + relationship's last role. + """ + if expr is None: + expr = EntityMapping(self._model._last_role_player(), **identifier) + if isinstance(expr, EntityMapping): + return expr.ref(), expr.constraints() + if isinstance(expr, ValueMapping): + return expr.expr(), expr.constraints() + return expr, [] diff --git a/converters/ontology/src/ossie_ontology/vendor/relationalai/ontology.py b/converters/ontology/src/ossie_ontology/vendor/relationalai/ontology.py new file mode 100644 index 00000000..99705fba --- /dev/null +++ b/converters/ontology/src/ossie_ontology/vendor/relationalai/ontology.py @@ -0,0 +1,309 @@ +# 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 warnings + +from relationalai.semantics import Model as RAIModel, Concept as RAIConcept, Chain +from relationalai.semantics import select, Relationship as RAIRelationship, Table as RAITable +from relationalai.semantics.frontend.base import Ref, CoreConcepts, NumberConcept +from relationalai.semantics.frontend.core import is_primitive_concept, Float + +from ossie_ontology.vendor.relationalai.mappings import EBinding, GRBinding, JoinPathBinding +from ossie_ontology.vendor.relationalai.roles import role_player + + +# Given c, which is either a PyRel Concept or a Ref, return either +# c or the Concept that c references +def concept_from_ref(c): + return c._concept if isinstance(c, Ref) else c + +class OntologyModel: + + def __init__(self, m): + self._delegate = RAIModel(m) + self._concept = {} + self._concept_module = {} + self._properties = {} + self._super_type_of = {} + self._edges = [] + self._table_index = {} + + def base_model(self): + return self._delegate + + def Concept(self, name, extends = None): + if extends is None: + extends = [] + c = self._delegate.Concept(name, extends=extends) + key = self._concept_key(c) + self._concept[key] = c + self._properties[key] = [] + if not is_primitive_concept(c): + if len(extends) > 0: + if len(extends) > 1: + warnings.warn(f"Assuming first supertype of {name} provides inherited identifier") + self._super_type_of[name] = extends[0] + return c + + # If provided, stype must be a RAIConcept that provides (at least part of) + # the identifier + def EntityBinding(self, e: RAIConcept, stype: RAIConcept | None = None, **identifier): + return EBinding(self, e, stype, **identifier) + + def Property(self, madlib, short_name: str | None = None): + # pyrel annotates `short_name` as `str` and defaults it to "", but it + # also uses the value as a key in `relationship_index`, so None and "" + # are not interchangeable. Pass what the caller gave us unchanged. + p = self._delegate.Property(madlib, short_name=short_name) # type: ignore[arg-type] + + if self._is_binary(p): + src_key = self._role_key(self._source_role(p)) + if src_key in self._concept: + self._properties[src_key].append(p) + + if self._is_edge(p): + self._log_edge(p) + return p + + def Relationship(self, madlib, short_name: str | None = None): + # pyrel annotates `short_name` as `str` and defaults it to "", but it + # also uses the value as a key in `relationship_index`, so None and "" + # are not interchangeable. Pass what the caller gave us unchanged. + r = self._delegate.Relationship(madlib, short_name=short_name) # type: ignore[arg-type] + if self._is_edge(r): + self._log_edge(r) + return r + + def RelationshipBinding(self, *emaps): + return GRBinding(self, *emaps) + + def JoinPath(self, r: RAIRelationship): + return JoinPathBinding(self, r) + + def query(self, c: RAIConcept): + """Return a PyRel select of all properties of concept *c*.""" + key = self._concept_key(c) + if key is None: + raise ValueError("Fail") + + params = [] + schemes = self.ref_schemes(c) + if schemes: + for rel in schemes: + params.append(Chain(c, rel).alias(rel._short_name)) + + if key in self._properties: + props = self._properties[key] + for prop in props: + params.append(self._readable_relationship_expr(c, prop)) + + return select(*params) + + def declares_preferred_identifier(self, c: RAIConcept): + return self.ref_schemes(c, shallow=True) is not None + + def define(self, *args): + return self._delegate.define(*args) + + def not_(self, *args): + return self._delegate.not_(*args) + + def where(self, *args): + return self._delegate.where(*args) + + def ref_schemes(self, concept: RAIConcept, shallow: bool = False) -> tuple[RAIRelationship, ...] | None: + seen_ids: set[int] = set() + ref_schema: list[RAIRelationship] = [] + + def add_ref_scheme_component(cand: RAIRelationship) -> None: + rel_id = cand._id + if rel_id not in seen_ids: + seen_ids.add(rel_id) + ref_schema.append(cand) + + if not shallow: + for parent in concept._extends: + parent_schema = self.ref_schemes(parent) + if parent_schema: + for rel in parent_schema: + add_ref_scheme_component(rel) + for rel in concept._identify_by: + add_ref_scheme_component(rel) + return tuple(ref_schema) if ref_schema else None + + def readable_dot_join(self, h, rel): + expr = Chain(h, rel) + ref_schemes = self.ref_schemes(role_player(self._target_role(rel))) + if not ref_schemes: + return expr + + rel_name = rel._short_name + warnings.warn( + "Attempting to dot join on a composite entity type, joining on the first component of the ref scheme") + return Chain(expr, ref_schemes[0]).alias(rel_name) + + # Returns a pair (Comp, G) where Comp is a new Concept named *name* that represents + # weakly connected components of nodes of type C. The caller is expected to then + # populate the edge relation of G. Comp declares several properties, including: + # - Comp.equates(C), which connects Comp to each C node it groups, and + # - Comp.size, which is the size of Comp.equates for a given Comp. + def find_path_schemas(self, start: RAIConcept, end, depth=4): + initial_term_path = {} + term_key = self._concept_key(concept_from_ref(end)) + + key = self._concept_key(start) + if key not in self._concept_module: + raise ValueError(f"Cannot find paths starting at concept {start}. Try using an entity type") + + for r in self._concept_module[key]: + path_end = self._role_key(self._target_role(r)) + + # Prune paths that do not terminate in the ending concept + if depth <= 1 and path_end != term_key: + continue + + if path_end not in initial_term_path: + initial_term_path[path_end] = [] + initial_term_path[path_end].append([r]) + + term_path = self._recursive_find_path_schemas(initial_term_path, term_key, depth - 1) + + paths = [] + for v in term_path.values(): + for p in v: + paths.append(p) + return paths + + def add_table(self, name: str, table): + # Deliberately untyped: whatever a table provider returns. The default + # yields a pyrel `Table`, but an extension may supply anything with the + # same column-accessor surface, such as a CSV-backed stand-in. + self._table_index[name] = table + + def lookup_concept(self, name: str) -> RAIConcept | None: + if name in CoreConcepts: + return CoreConcepts[name] + else: + return self.base_model().concept_index.get(name) + + def lookup_table(self, name: str) -> RAITable | None: + return self._table_index.get(name) + + def lookup_relationship(self, name: str) -> RAIRelationship | None: + return self.base_model().relationship_index.get(name) + + def get_topmost_parent(self, concept: RAIConcept) -> RAIConcept: + if not concept._extends or isinstance(concept, NumberConcept) or concept is Float: + return concept + return self.get_topmost_parent(concept._extends[0]) + + def _recursive_find_path_schemas(self, term_map, term_key, depth): + if depth == 0: + return term_map + new_term_map = {} + for c, paths in term_map.items(): + for path in paths: + for r in self._concept_module[c]: + self._extend_path_schema( + new_term_map, path, r, term_key, depth) + return self._recursive_find_path_schemas(new_term_map, term_key, depth - 1) + + def _extend_path_schema(self, new_term_map, path, r, term_key, depth): + """Append ``path + [r]`` to *new_term_map* under the key of r's target + role, unless the target wouldn't reach *term_key* on the next pass. + """ + path_end = self._role_key(self._target_role(r)) + # Prune paths that do not terminate in the ending concept + if depth <= 1 and path_end != term_key: + return + new_term_map.setdefault(path_end, []).append(path + [r]) + + # Given a Relationship r whose facts should represent edges in the graph, + # log r as an edge so that we can later do path analysis using it. + def _log_edge(self, r): + self._edges.append(r) + # + src_key = self._role_key(self._source_role(r)) + if src_key not in self._concept_module: + self._concept_module[src_key] = [] + self._concept_module[src_key].append(r) + + # Given a binary relationship rel, returns an expression that allows rel to be + # queried so that it returns a readable answer when used in a select query. + def _readable_relationship_expr(self, c, rel): + schemes = self.ref_schemes(role_player(self._target_role(rel))) + rel_name = rel._short_name + + if not schemes: + return Chain(c, rel).alias(rel_name) + + first_scheme = schemes[0] + if len(schemes) > 1: + warnings.warn( + "Attempting to join on a composite entity type, joining on the first component of the ref scheme") + return Chain(Chain(c, rel), first_scheme).alias(rel_name) + + def _concept_key(self, c): + if isinstance(c, RAIConcept): + return c._name.lower() + else: + return c.lower() + + def _is_value_type(self, c: RAIConcept): + return is_primitive_concept(c) + + def _is_entity_type(self, c: RAIConcept): + return not self._is_value_type(c) + + def _is_entity_role(self, role): + key = self._role_key(role) + if key in self._concept and self._is_entity_type(self._concept[key]): + return True + return False + + def _is_binary(self, r): + return len(r._fields) == 2 + + def _is_edge(self, r): + if (self._is_binary(r) and self._is_entity_role(self._source_role(r)) and + self._is_entity_role(self._target_role(r))): + return True + return False + + def _last_role(self, r): + return r._fields[len(r._fields) - 1] + + def _last_role_player(self, r): + return self._role_player(self._last_role(r)) + + def _role_key(self, role): + return self._concept_key(role.type._name) + + def _role_player(self, r): + return self._concept[self._role_key(r)] + + def _source_role(self, r): + return r._fields[0] + + def _source_role_player(self, r): + return self._role_player(self._source_role(r)) + + def _target_role(self, r): + return r._fields[1] + + def _target_role_player(self, r): + return self._role_player(self._target_role(r)) diff --git a/converters/ontology/src/ossie_ontology/vendor/relationalai/roles.py b/converters/ontology/src/ossie_ontology/vendor/relationalai/roles.py new file mode 100644 index 00000000..aaacd1a2 --- /dev/null +++ b/converters/ontology/src/ossie_ontology/vendor/relationalai/roles.py @@ -0,0 +1,100 @@ +# 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. + +""" +Utilities for working with PyRel FieldRef (role) objects. + +PyRel roles are accessed via rel[field] syntax and return a FieldRef. +FieldRef._root may be a Relationship, Chain, or Reading, so direct +attribute access on _root is not safe without going through these helpers. +""" +from relationalai.semantics import Concept, Relationship, Chain, Reading +from relationalai.semantics.frontend.base import FieldRef, Field + + +def role_part_of(role: FieldRef) -> Relationship: + """Return the Relationship that this role belongs to.""" + cand = role._root + if isinstance(cand, Relationship): + return cand + elif isinstance(cand, Chain): + return cand._next + else: + raise ValueError(f"Unexpected root type: {type(cand)}") + + +def role_player(role: Field|FieldRef) -> Concept: + """Return the Concept that plays this role.""" + return role._to_concept() if isinstance(role, FieldRef) else role.type + + +def role_guid(role: FieldRef) -> int: + """ + Return a stable, hashable identifier for a role. + Safe to use as a dict key across multiple FieldRef instances + that refer to the same role (unlike FieldRef._id, which is + per-instance, not per-role). + """ + rel = role_part_of(role) + if isinstance(rel, Reading): + rel = rel._relationship + field = role._resolved + role_ix = rel._fields.index(field) + return hash((role_ix, rel_guid(rel))) + + +def rel_guid(rel: Relationship | Chain) -> int: + """ + Return a stable, hashable identifier for a relationship, suitable for + structural equality and dict/set keys. Derived from the reading used by + role_guid so that role_guid and rel_guid stay in agreement. + """ + if isinstance(rel, Chain): + rel = rel._next + if isinstance(rel, Reading): + rel = rel._relationship + return hash(rel._readings[0]._reading) + + +def role_siblings(role: FieldRef) -> list[FieldRef]: + """Return all sibling roles (all other roles of the same relationship).""" + rel = role_part_of(role) + return [rel[idx] for idx, _ in enumerate(rel._fields) if idx != role._resolved_ix] + + +def role_sibling(role: FieldRef) -> FieldRef: + """Return the single sibling role. Asserts the relationship is binary.""" + siblings = role_siblings(role) + assert len(siblings) == 1, f"Expected 1 sibling, got {len(siblings)}" + return siblings[0] + + +def role_eq(role1: FieldRef, role2: FieldRef) -> bool: + """Return True if two FieldRefs refer to the same role.""" + return role_guid(role1) == role_guid(role2) + + +def role_pprint(role: FieldRef) -> str: + """Return a human-readable string for a role.""" + part_of = role_part_of(role) + if isinstance(part_of, Reading): + reading = part_of._reading + elif isinstance(part_of, Relationship): + reading = part_of._readings[0]._reading + else: + raise ValueError(f"Unexpected root type: {type(part_of)}") + return f"{reading}[{role._resolved_ix}]" diff --git a/converters/ontology/tests/common/__init__.py b/converters/ontology/tests/common/__init__.py new file mode 100644 index 00000000..4752fbd4 --- /dev/null +++ b/converters/ontology/tests/common/__init__.py @@ -0,0 +1,18 @@ +# 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. + +"""Utilities with no ontology in them: the graph sort and the text helpers.""" diff --git a/converters/ontology/tests/test_graph.py b/converters/ontology/tests/common/test_graph.py similarity index 100% rename from converters/ontology/tests/test_graph.py rename to converters/ontology/tests/common/test_graph.py diff --git a/converters/ontology/tests/test_verbalization_strings.py b/converters/ontology/tests/common/test_text_utils.py similarity index 100% rename from converters/ontology/tests/test_verbalization_strings.py rename to converters/ontology/tests/common/test_text_utils.py diff --git a/converters/ontology/tests/config/raiconfig.yaml b/converters/ontology/tests/config/raiconfig.yaml new file mode 100644 index 00000000..be047927 --- /dev/null +++ b/converters/ontology/tests/config/raiconfig.yaml @@ -0,0 +1,50 @@ +# 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. + +# A deliberately offline RelationalAI config for the test suite. +# +# `relationalai` builds its Config the moment a Model is constructed, and it +# refuses to start without one -- even for work that never leaves the process, +# which is all this suite does (build a model, compile it to the metamodel, +# decompile it back to PyRel source). conftest.py pins RAI_CONFIG_FILE_PATH to +# this file so the tests cannot pick up a contributor's real raiconfig.yaml and +# quietly start talking to their warehouse. +# +# The connection below exists only because the config schema requires at least +# one. The credentials are placeholders and are never used: a test that reaches +# a warehouse is a test that is doing something it should not, and it will fail +# to authenticate rather than succeed silently. +active_profile: offline + +model: + implicit_properties: false + +compiler: + dry_run: true + +connections: + offline: + type: snowflake + account: not-a-real-account + user: offline + password: offline + warehouse: offline + role: offline + +profiles: + offline: + default_connection: offline diff --git a/converters/ontology/tests/conftest.py b/converters/ontology/tests/conftest.py index 947c4eb0..d063a55a 100644 --- a/converters/ontology/tests/conftest.py +++ b/converters/ontology/tests/conftest.py @@ -19,6 +19,7 @@ from __future__ import annotations +import os from pathlib import Path import pytest @@ -30,7 +31,17 @@ # repo-level examples/ directory isn't present (e.g. from an sdist/wheel or a # subset checkout). tests/test_examples_in_sync.py guards against drift from the # canonical examples/ copies. -_FIXTURES_DIR = Path(__file__).resolve().parent / "fixtures" +# +# Sorted by what a file is for, because the suites read them differently: +# specs/ full ontologies the snapshot suites render end to end +# behaviours/ one spec per behaviour, asserted directly rather than snapshotted +# validation/ formulas that must parse, or must fail with a stated message +# palantir/ a vendor export, in that vendor's own layout +# Harness configuration is not a test input and lives in tests/config/. +_TESTS_DIR = Path(__file__).resolve().parent +_FIXTURES_DIR = _TESTS_DIR / "fixtures" +_SPECS_DIR = _FIXTURES_DIR / "specs" +_BEHAVIOURS_DIR = _FIXTURES_DIR / "behaviours" # The snapshot files under tests/snapshots/ carry the ASF license header inline, # so that a source-release audit (Apache RAT) finds a header on every file in the @@ -86,16 +97,99 @@ def assert_match_with_license(value: str | bytes, snapshot_name: str | Path) -> return snapshot +# `relationalai` resolves its config the first time a Model is constructed, and +# searches the CWD, ~/.rai, ~/.snowflake and ~/.dbt in turn. On a contributor's +# machine that search usually finds a real, credentialed config — so the Ossie -> +# RelationalAI tests would build their models against a live warehouse without +# anyone noticing. Pinning the variable at import time, before any test can +# construct a Model, forces the offline fixture instead. +# +# Set unconditionally: the pin matters most on exactly the machines where a real +# config exists, and the tests never need one. +os.environ["RAI_CONFIG_FILE_PATH"] = str(_TESTS_DIR / "config" / "raiconfig.yaml") + + @pytest.fixture(scope="session") def fixtures_dir() -> Path: return _FIXTURES_DIR +@pytest.fixture(scope="session") +def specs_dir() -> Path: + """Full ontologies: flights, retail, tpch.""" + return _SPECS_DIR + + +@pytest.fixture(scope="session") +def behaviours_dir() -> Path: + """One spec per behaviour, each asserted on directly.""" + return _BEHAVIOURS_DIR + + +@pytest.fixture(scope="session") +def validation_dir() -> Path: + """Formulas that must parse, or must fail with a stated message.""" + return _FIXTURES_DIR / "validation" + + @pytest.fixture(scope="session") def flights_path(fixtures_dir: Path) -> Path: - return fixtures_dir / "flights.yaml" + """The reference corpus: read by the spec snapshots, the PyRel snapshots, the + parser round-trip tests, and test_reasoner for its composite reference scheme + (Runway is identified by designator *and* airport). + + Its note lives here rather than in the file, because the file is the vendored + copy of examples/flights.yaml and test_examples_in_sync holds the two + byte-identical -- retail.yaml and tpch.yaml are test-only and carry theirs + inline. + """ + return _SPECS_DIR / "flights.yaml" @pytest.fixture def flights_model(flights_path: Path) -> OssieOntology: - return OssieParser().parse(flights_path) \ No newline at end of file + return OssieParser().parse(flights_path) + +# The corpus the snapshot suites run over, and the single source of that list -- +# tests/relationalai/test_converter.py imports it rather than repeating it. `flights` +# also keeps the named fixtures above, since several tests assert on it directly. +SPEC_NAMES = ["flights", "retail", "tpch"] + + +@pytest.fixture +def spec_model(request) -> OssieOntology: + """The parsed ontology for the spec named by an indirect parametrize. + + Formulas stay as raw text: these suites snapshot the shape of the spec, and + parsing them would pull the optional `relationalai` extra in through the + formula factories for no gain. + """ + return OssieParser().parse(_SPECS_DIR / f"{request.param}.yaml") + + +# `relationalai` resolves its config when a Model is constructed and will not +# start without one, so the pin above is load-bearing, not belt-and-braces -- +# removing it does not make the suite offline, it makes it fail. What keeps the +# suite offline is that nothing it does needs a warehouse: models are compiled +# to the metamodel in-process and decompiled back to source. +# +# That is a property worth failing on rather than documenting. Every socket +# connection is refused for the duration of a test, so a test that reaches for a +# warehouse -- by using the default `warehouse_table` provider, say, or its own +# config -- says so immediately instead of quietly succeeding on the machine of +# whoever happens to have credentials. +@pytest.fixture(autouse=True) +def _no_network(monkeypatch): + import socket + + def deny(*args, **kwargs): + raise RuntimeError( + "This suite runs offline: a test tried to open a network connection. " + "Conversion and compilation happen in-process; if you need a table, " + "declare its columns with `declared_table` rather than reading them " + "from a warehouse." + ) + + monkeypatch.setattr(socket.socket, "connect", deny) + monkeypatch.setattr(socket.socket, "connect_ex", deny) + monkeypatch.setattr(socket, "create_connection", deny) diff --git a/converters/ontology/tests/expr/__init__.py b/converters/ontology/tests/expr/__init__.py new file mode 100644 index 00000000..c6c409b5 --- /dev/null +++ b/converters/ontology/tests/expr/__init__.py @@ -0,0 +1,18 @@ +# 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. + +"""Formulas: the grammar that parses them and the rules that reject them.""" diff --git a/converters/ontology/tests/expr/test_parsing.py b/converters/ontology/tests/expr/test_parsing.py new file mode 100644 index 00000000..ecbf6652 --- /dev/null +++ b/converters/ontology/tests/expr/test_parsing.py @@ -0,0 +1,237 @@ +# 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. + +"""End-to-end coverage for the formula parser, validator, and the two factories +that plug them into `OssieParser`. + +`FormulaFactory` and `MappingFormulaFactory` in `ossie_ontology.model` keep a +formula's raw text and nothing else. Passing the parsing factories from +`ossie_ontology.expr.factory` to `OssieParser` swaps in the real thing: the +expression is lexed, parsed into an AST, and validated against the ontology (and, +for mapping expressions, against the semantic model's dataset columns). Every +case below goes through `OssieParser`, so it covers that whole path rather than +the validator alone — `test_formula_validator.py` handles the one check that +cannot be reached this way. +""" + +from __future__ import annotations + +import re +from pathlib import Path + +import pytest + +from ossie_ontology.model import Formula, OssieOntology +from ossie_ontology.parser import OssieParser + + + +def parse(validation_dir: Path, fixture: str) -> OssieOntology: + """Parse a fixture. `OssieParser` parses and validates formulas by default.""" + return OssieParser().parse(validation_dir / fixture) + + +# --------------------------------------------------------------------------- +# Rule formulas: `derived_by` and `requires` +# --------------------------------------------------------------------------- + +RULE_ERRORS = [ + ( + "ambiguous_type_vars.yaml", + "Relationship 'ssn' signature mismatch in the formula " + "'Person.earns(var_name) AND Person.ssn(var_name)': " + "expected (Person, SocialSecurityNr) got (Person, Salary)", + ), + ( + "undefined_relationship.yaml", + "Undefined relationship 'Person.gain' in the formula 'Person.gain(Salary)'", + ), + ( + "unresolved_var_type.yaml", + "Unresolved reference 's' in the formula 's > 0 AND Person.earns(Salary)'. " + "If 's' is a dataset field, use the format 'DATASET_NAME.s'.", + ), + ( + "not_declared_concept_in_relationship.yaml", + "Role concept 'City' in relationship 'Person.live_in' is not declared in the ontology.", + ), + ( + "not_declared_concept_in_identifier.yaml", + "identify_by 'ssn' on concept 'Person' refers to an unknown relationship " + "in ontology 'Sample model'.", + ), + ( + "signature_mismatch.yaml", + "Relationship 'ssn' signature mismatch in the formula " + "'Person.earns(Salary) AND Person.ssn(Salary)': " + "expected (Person, SocialSecurityNr) got (Person, Salary)", + ), + ( + "bare_field_in_formula.yaml", + "Unresolved reference 'sal' in the formula 'Person.earns(Salary(sal))'. " + "If 'sal' is a dataset field, use the format 'DATASET_NAME.sal'.", + ), + ( + "top_level_require_unknown_concept.yaml", + "Unresolved reference 'Nonexistent' in the formula 'COUNT[Nonexistent] > 0'. " + "If 'Nonexistent' is a dataset field, use the format 'DATASET_NAME.Nonexistent'.", + ), + ( + "top_level_require_free_var.yaml", + "Unbound reference(s) 'Person' in the ontology-level constraint 'Person.ssn > 0'. " + "An ontology-level `requires` has no parent scope to bind variables against, so " + "every reference must be bound within the formula — by an aggregate (e.g. COUNT[...]) " + "or by wrapping it in EXISTS(...), e.g. 'EXISTS(Person.ssn > 0)'.", + ), + ( + "require_sub_exists_unknown_rel.yaml", + "Undefined relationship 'Company.turnover' in the formula " + "'Person.name AND EXISTS(Company.turnover > 0)'", + ), +] + + +@pytest.mark.parametrize("fixture,message", RULE_ERRORS, ids=[c[0] for c in RULE_ERRORS]) +def test_invalid_rule_formula_is_rejected(validation_dir: Path, fixture: str, message: str): + with pytest.raises(ValueError, match=re.escape(message)): + parse(validation_dir, fixture) + + +@pytest.mark.parametrize( + "fixture", + [ + # An ontology-level `requires` has no parent scope, but is still parsed + # and validated — here a bare COUNT aggregate over a concept. + "top_level_require_valid.yaml", + # Free variables in a top-level `requires` become legal once EXISTS(...) + # binds them. + "top_level_require_exists_valid.yaml", + # A sub-EXISTS inside a concept- or relationship-level `require` (parent + # scope is not None): the concept require mixes a head-scoped term with an + # EXISTS over another concept, the relationship require is a bare EXISTS. + # The EXISTS body is fully validated either way. + "require_sub_exists_valid.yaml", + # Scope binding in depth: a second concept reached through a fact is + # bound without EXISTS, in either conjunct order; nested binders bind + # what they enclose; scope travels along a chain of facts. The fixture + # documents each case inline; nothing referenced it before, so this is + # the first thing that runs it. + "scoped_require_scope_valid.yaml", + ], +) +def test_valid_rule_formula_parses(validation_dir: Path, fixture: str): + assert parse(validation_dir, fixture) is not None + + +# --------------------------------------------------------------------------- +# Mapping formulas +# --------------------------------------------------------------------------- + +MAPPING_ERRORS = [ + ( + "mapping_formula_unknown_field.yaml", + "Undefined column 'unknownField' for dataset 'PAYMENTS'", + ), + ( + "mapping_formula_agg_with_rel.yaml", + "Relationship references are not allowed in mapping expressions: 'grossReturns' " + "in 'SUM[SALES.amount WHERE Item.grossReturns(Store) > 0 GROUP BY SALES.item_id]'.", + ), + ( + "mapping_formula_concept_ref.yaml", + "Concept references are not allowed in mapping expressions: 'Amount' " + "in 'PAYMENTS.cashReceived + Amount'.", + ), +] + + +@pytest.mark.parametrize("fixture,message", MAPPING_ERRORS, ids=[c[0] for c in MAPPING_ERRORS]) +def test_invalid_mapping_formula_is_rejected(validation_dir: Path, fixture: str, message: str): + with pytest.raises(ValueError, match=re.escape(message)): + parse(validation_dir, fixture) + + +def test_mapping_formula_reaches_the_object_mapping_as_a_formula(validation_dir: Path): + """A mapped property backed by an expression carries a parsed Formula. + + Under the plain `MappingFormulaFactory` the same field arrives as a bare + `Formula` with no AST, so this asserts the parsing factory is the one the + parser reached for by default. + """ + model = parse(validation_dir, "mapping_formula_valid.yaml") + + mapping, = model.ontology_mappings + assert mapping.concept_mappings is not None + concept_mapping, = mapping.concept_mappings + assert concept_mapping.concept is not None + assert concept_mapping.concept.name == "CashPayment" + + link_mapping, = concept_mapping.link_mappings + assert link_mapping.children is not None + child, = link_mapping.children + assert child.relationship is not None + assert child.relationship.name == "netAmount" + + object_mapping = child.object_mapping + assert object_mapping is not None + assert isinstance(object_mapping.expression, Formula) + assert object_mapping.expression.raw_expr == "PAYMENTS.cashReceived - PAYMENTS.change" + + +# --------------------------------------------------------------------------- +# The default wiring +# --------------------------------------------------------------------------- + +def test_parsing_is_the_default(validation_dir: Path): + """A bare `OssieParser()` parses and validates formulas. + + It used to keep them as raw text unless a caller passed the factories in, + which meant an invalid formula parsed cleanly and was then skipped by every + downstream converter — the error surfaced as a missing rule, if at all. + """ + from ossie_ontology.expr.formula.model import Formula as ParsedFormula + from ossie_ontology.parser import OssieParser + + model = OssieParser().parse(validation_dir / "top_level_require_valid.yaml") + formulas = [f for c in model.ontology.concepts(exclude_builtin=True) for f in c.requires] + formulas += list(model.ontology.requires) + assert formulas, "fixture should declare at least one constraint" + assert all(isinstance(f, ParsedFormula) for f in formulas) + + +def test_default_parser_rejects_an_invalid_formula(validation_dir: Path): + """The validation above is reached without the caller opting in.""" + from ossie_ontology.parser import OssieParser + + with pytest.raises(ValueError, match=re.escape("Undefined relationship 'Person.gain'")): + OssieParser().parse(validation_dir / "undefined_relationship.yaml") + + +def test_raw_factories_remain_available_as_an_opt_out(validation_dir: Path): + """Passing the plain factories still skips parsing entirely.""" + from ossie_ontology.model import Formula, FormulaFactory, MappingFormulaFactory + from ossie_ontology.parser import OssieParser + + # Would raise under the default factories; the raw ones keep it as text. + model = OssieParser( + formula_factory=FormulaFactory(), + mapping_formula_factory=MappingFormulaFactory(), + ).parse(validation_dir / "undefined_relationship.yaml") + + # The fixture's formula is a relationship `derived_by`, not a concept rule. + formulas = [f for r in model.ontology.relationships for f in (*r.derived_by, *r.requires)] + assert formulas and all(type(f) is Formula for f in formulas) diff --git a/converters/ontology/tests/expr/test_validator.py b/converters/ontology/tests/expr/test_validator.py new file mode 100644 index 00000000..8e422568 --- /dev/null +++ b/converters/ontology/tests/expr/test_validator.py @@ -0,0 +1,71 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +import pytest + +from ossie_ontology.model import Concept, ConceptType, OntologyComponent, Relationship +from ossie_ontology.expr.formula.model import ConceptRefHandle, DotJoinHandle, RelationshipRefHandle +from ossie_ontology.expr.formula.validator import FormulaValidator +from ossie_ontology.reasoner import OntologyReasoner + + +class TestFormulaValidatorDotJoin: + """FormulaValidator._validate_dot_join's container type check can't be reached through + the OSI parser: a dotted relationship reference only resolves at parse time when the + relationship is actually declared on the left-hand concept (or one of its ancestors), + which already guarantees the check would pass. These tests call the validator directly + with a hand-built AST to exercise the check in isolation.""" + + # `_validate_dot_join` only consults the ontology on its VarHandle branch, + # which these cases do not take — but passing None to a parameter typed + # `OntologyComponent` says that badly. An empty component is just as inert + # and keeps the call honest. + def _reasoner_with(self, *concepts: Concept) -> OntologyReasoner: + reasoner = OntologyReasoner() + for concept in concepts: + reasoner.new_concept(concept) + return reasoner + + def test_dot_join_container_type_mismatch_raises(self): + person = Concept(name="Person", type=ConceptType.ENTITY_TYPE) + salary = Concept(name="Salary", type=ConceptType.VALUE_TYPE) + company = Concept(name="Company", type=ConceptType.ENTITY_TYPE) # unrelated to Person + earns = Relationship(name="earns", container=person, relates=[(salary, None)]) + + reasoner = self._reasoner_with(person, salary, company) + dot_join = DotJoinHandle(ConceptRefHandle(company), RelationshipRefHandle(earns)) + + with pytest.raises( + ValueError, + match=( + r"Type mismatch for 'Company\.earns' in the formula 'Company\.earns'\. " + r"Expected type in the relationship 'Person' got 'Company'" + ), + ): + FormulaValidator._validate_dot_join(OntologyComponent(), reasoner, [dot_join], {}, "Company.earns") + + def test_dot_join_container_subtype_is_allowed(self): + person = Concept(name="Person", type=ConceptType.ENTITY_TYPE) + salary = Concept(name="Salary", type=ConceptType.VALUE_TYPE) + employee = Concept(name="Employee", type=ConceptType.ENTITY_TYPE, extends=[person]) + earns = Relationship(name="earns", container=person, relates=[(salary, None)]) + + reasoner = self._reasoner_with(person, salary, employee) + dot_join = DotJoinHandle(ConceptRefHandle(employee), RelationshipRefHandle(earns)) + + # Should not raise: Employee is a subtype of earns' container (Person). + FormulaValidator._validate_dot_join(OntologyComponent(), reasoner, [dot_join], {}, "Employee.earns") \ No newline at end of file diff --git a/converters/ontology/tests/fixtures/behaviours/derived_identifier.yaml b/converters/ontology/tests/fixtures/behaviours/derived_identifier.yaml new file mode 100644 index 00000000..a936bab8 --- /dev/null +++ b/converters/ontology/tests/fixtures/behaviours/derived_identifier.yaml @@ -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. + +# A concept whose *identifier* is derived rather than loaded: Product is +# identified by `sku`, and `sku` is computed from Legacy's code. +# +# This shape is what separates the two branches of the PyRel emitter's `_head`. +# A derived identifier must emit `Product.new(sku=...)`, which brings the entity +# into existence; the non-identifier branch emits `Product.sku(...)`, a fact +# about an entity nothing ever created. No other fixture in either repo pairs +# `identify_by` with `derived_by`, so without this one the identifier branch has +# no coverage at all. +version: 0.2.0.dev0 +name: DerivedIdentifier +ontology: +- concept: Code + type: ValueType + extends: [String] +- concept: Legacy + type: EntityType + identify_by: [legacyCode] + relationships: + - name: legacyCode + roles: + - concept: Code + name: c + verbalizes: ['{Legacy} has {Code:c}'] + multiplicity: OneToOne +- concept: Product + type: EntityType + identify_by: [sku] + relationships: + - name: sku + roles: + - concept: Code + name: c + verbalizes: ['{Product} has {Code:c}'] + multiplicity: OneToOne + derived_by: ['Legacy.legacyCode(c)'] diff --git a/converters/ontology/tests/fixtures/behaviours/key_column_both_ways.yaml b/converters/ontology/tests/fixtures/behaviours/key_column_both_ways.yaml new file mode 100644 index 00000000..ba2964ab --- /dev/null +++ b/converters/ontology/tests/fixtures/behaviours/key_column_both_ways.yaml @@ -0,0 +1,179 @@ +# 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. + +# One column keying an entity through both mapping styles at once. +# +# `SALES.storeNr` is read twice: once as a bare `expression` on the soldAt link +# node, and once through a `referent_mappings` entry keying Store by `nr`. Both +# say the column holds a store number, so both must pin the field to `StoreNr`. +# +# They did not agree. The referent path took the type from the relationship's +# last role, the object-mapping path took it from `concept:` -- the entity -- +# and the two spellings of one fact collided with "Field 'storeNr' is already +# mapped as concept 'StoreNr' but this mapping expects 'Store'". +# +# Otherwise identical to ref_scheme.yaml, which this was derived from: +# +# * Store is mapped with `expression:` and no `referent_mappings`, so the +# converter has to ask the reasoner which relationship identifies it before +# it can name the keyword argument of `Store.new(...)`. +# * Sale -> soldAt -> Store is a bare-expression link node that carries +# children, so the converter builds a child binding scoped to the Store the +# expression identifies, again via the ref scheme. +# * LargeSale is derived and extends nothing, so the formula emitter takes its +# ref-scheme branch rather than the subtype branch that CardPayment in +# retail.yaml covers. +version: 0.2.0.dev0 +name: KeyColumnBothWays +ontology: +- concept: StoreNr + type: ValueType + extends: [Integer] +- concept: Region + type: ValueType + extends: [String] +- concept: SaleNr + type: ValueType + extends: [Integer] +- concept: Amount + type: ValueType + extends: [Decimal] +- concept: Store + type: EntityType + identify_by: [nr] + relationships: + - name: nr + roles: + - concept: StoreNr + name: n + verbalizes: ['{Store} has {StoreNr:n}'] + multiplicity: OneToOne + - name: region + roles: + - concept: Region + name: r + verbalizes: ['{Store} is in {Region:r}'] + multiplicity: ManyToOne +- concept: Sale + type: EntityType + identify_by: [nr] + relationships: + - name: nr + roles: + - concept: SaleNr + name: n + verbalizes: ['{Sale} has {SaleNr:n}'] + multiplicity: OneToOne + - name: soldAt + roles: + - concept: Store + name: s + verbalizes: ['{Sale} was sold at {Store:s}'] + multiplicity: ManyToOne + - name: amount + roles: + - concept: Amount + name: a + verbalizes: ['{Sale} totals {Amount:a}'] + multiplicity: ManyToOne +- concept: LargeSale + type: EntityType + identify_by: [nr] + derived_by: ['Sale.nr(n) AND Sale.amount(a) AND a > 1000'] + relationships: + - name: nr + roles: + - concept: SaleNr + name: n + verbalizes: ['{LargeSale} has {SaleNr:n}'] + multiplicity: OneToOne +ontology_mappings: +- name: ref_scheme_mapping + semantic_model: + name: Ref scheme semantic model + datasets: + - name: STORES + source: DB.SCHEMA.STORES + fields: + - name: storeNr + expression: + dialects: + - dialect: ANSI_SQL + expression: storeNr + - name: region + expression: + dialects: + - dialect: ANSI_SQL + expression: region + - name: SALES + source: DB.SCHEMA.SALES + fields: + - name: saleNr + expression: + dialects: + - dialect: ANSI_SQL + expression: saleNr + - name: storeNr + expression: + dialects: + - dialect: ANSI_SQL + expression: storeNr + - name: storeRegion + expression: + dialects: + - dialect: ANSI_SQL + expression: storeRegion + - name: amount + expression: + dialects: + - dialect: ANSI_SQL + expression: amount + concept_mappings: + - concept: Store + object_mappings: + - expression: STORES.storeNr + - referent_mappings: + - relationship: nr + expression: SALES.storeNr + link_mappings: + - object_mapping: + expression: STORES.storeNr + children: + - object_mapping: + concept: Region + expression: STORES.region + relationship: region + - concept: Sale + link_mappings: + - object_mapping: + referent_mappings: + - relationship: nr + expression: SALES.saleNr + children: + - object_mapping: + concept: Amount + expression: SALES.amount + relationship: amount + - object_mapping: + concept: Store + expression: SALES.storeNr + relationship: soldAt + children: + - object_mapping: + concept: Region + expression: SALES.storeRegion + relationship: region diff --git a/converters/ontology/tests/fixtures/behaviours/ref_scheme.yaml b/converters/ontology/tests/fixtures/behaviours/ref_scheme.yaml new file mode 100644 index 00000000..03f1a897 --- /dev/null +++ b/converters/ontology/tests/fixtures/behaviours/ref_scheme.yaml @@ -0,0 +1,167 @@ +# 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. + +# Entities identified by a bare `expression` mapping, plus a derived concept +# that is not a subtype. Both shapes send the converter through +# `OntologyReasoner.ref_scheme`, which no other fixture reaches: +# +# * Store is mapped with `expression:` and no `referent_mappings`, so the +# converter has to ask the reasoner which relationship identifies it before +# it can name the keyword argument of `Store.new(...)`. +# * Sale -> soldAt -> Store is a bare-expression link node that carries +# children, so the converter builds a child binding scoped to the Store the +# expression identifies, again via the ref scheme. +# * LargeSale is derived and extends nothing, so the formula emitter takes its +# ref-scheme branch rather than the subtype branch that CardPayment in +# retail.yaml covers. +version: 0.2.0.dev0 +name: RefScheme +ontology: +- concept: StoreNr + type: ValueType + extends: [Integer] +- concept: Region + type: ValueType + extends: [String] +- concept: SaleNr + type: ValueType + extends: [Integer] +- concept: Amount + type: ValueType + extends: [Decimal] +- concept: Store + type: EntityType + identify_by: [nr] + relationships: + - name: nr + roles: + - concept: StoreNr + name: n + verbalizes: ['{Store} has {StoreNr:n}'] + multiplicity: OneToOne + - name: region + roles: + - concept: Region + name: r + verbalizes: ['{Store} is in {Region:r}'] + multiplicity: ManyToOne +- concept: Sale + type: EntityType + identify_by: [nr] + relationships: + - name: nr + roles: + - concept: SaleNr + name: n + verbalizes: ['{Sale} has {SaleNr:n}'] + multiplicity: OneToOne + - name: soldAt + roles: + - concept: Store + name: s + verbalizes: ['{Sale} was sold at {Store:s}'] + multiplicity: ManyToOne + - name: amount + roles: + - concept: Amount + name: a + verbalizes: ['{Sale} totals {Amount:a}'] + multiplicity: ManyToOne +- concept: LargeSale + type: EntityType + identify_by: [nr] + derived_by: ['Sale.nr(n) AND Sale.amount(a) AND a > 1000'] + relationships: + - name: nr + roles: + - concept: SaleNr + name: n + verbalizes: ['{LargeSale} has {SaleNr:n}'] + multiplicity: OneToOne +ontology_mappings: +- name: ref_scheme_mapping + semantic_model: + name: Ref scheme semantic model + datasets: + - name: STORES + source: DB.SCHEMA.STORES + fields: + - name: storeNr + expression: + dialects: + - dialect: ANSI_SQL + expression: storeNr + - name: region + expression: + dialects: + - dialect: ANSI_SQL + expression: region + - name: SALES + source: DB.SCHEMA.SALES + fields: + - name: saleNr + expression: + dialects: + - dialect: ANSI_SQL + expression: saleNr + - name: storeNr + expression: + dialects: + - dialect: ANSI_SQL + expression: storeNr + - name: storeRegion + expression: + dialects: + - dialect: ANSI_SQL + expression: storeRegion + - name: amount + expression: + dialects: + - dialect: ANSI_SQL + expression: amount + concept_mappings: + - concept: Store + object_mappings: + - expression: STORES.storeNr + link_mappings: + - object_mapping: + expression: STORES.storeNr + children: + - object_mapping: + concept: Region + expression: STORES.region + relationship: region + - concept: Sale + link_mappings: + - object_mapping: + referent_mappings: + - relationship: nr + expression: SALES.saleNr + children: + - object_mapping: + concept: Amount + expression: SALES.amount + relationship: amount + - object_mapping: + concept: Store + expression: SALES.storeNr + relationship: soldAt + children: + - object_mapping: + concept: Region + expression: SALES.storeRegion + relationship: region diff --git a/converters/ontology/tests/fixtures/palantir/retail_mini/data_sets/ds.json b/converters/ontology/tests/fixtures/palantir/retail_mini/data_sets/ds.json new file mode 100644 index 00000000..931d3442 --- /dev/null +++ b/converters/ontology/tests/fixtures/palantir/retail_mini/data_sets/ds.json @@ -0,0 +1,34 @@ +[ + { + "mainDatasetId": "ri.ot.store", + "datasetName": "store_table", + "datasetSchema": [ + { + "name": "store_id", + "type": "STRING" + }, + { + "name": "store_name", + "type": "STRING" + } + ] + }, + { + "mainDatasetId": "ri.ot.sale", + "datasetName": "sale_table", + "datasetSchema": [ + { + "name": "sale_id", + "type": "STRING" + }, + { + "name": "amount", + "type": "STRING" + }, + { + "name": "store_id", + "type": "STRING" + } + ] + } +] diff --git a/converters/ontology/tests/fixtures/palantir/retail_mini/ontology.json b/converters/ontology/tests/fixtures/palantir/retail_mini/ontology.json new file mode 100644 index 00000000..35cf9c55 --- /dev/null +++ b/converters/ontology/tests/fixtures/palantir/retail_mini/ontology.json @@ -0,0 +1,75 @@ +{ + "objectTypes": [ + { + "rid": "ri.ot.store", + "id": "store", + "displayName": "Store", + "properties": [ + { + "rid": "ri.p.store_id", + "id": "store_id", + "baseType": { + "type": "STRING" + } + }, + { + "rid": "ri.p.store_name", + "id": "store_name", + "baseType": { + "type": "STRING" + } + } + ], + "primaryKeys": [ + "store_id" + ] + }, + { + "rid": "ri.ot.sale", + "id": "sale", + "displayName": "Sale", + "properties": [ + { + "rid": "ri.p.sale_id", + "id": "sale_id", + "baseType": { + "type": "STRING" + } + }, + { + "rid": "ri.p.amount", + "id": "amount", + "baseType": { + "type": "STRING" + } + }, + { + "rid": "ri.p.store_id", + "id": "store_id", + "baseType": { + "type": "STRING" + } + } + ], + "primaryKeys": [ + "sale_id" + ] + } + ], + "relations": [ + { + "rid": "ri.rel.sale_store", + "id": "sale_store", + "definition": { + "type": "oneToMany", + "oneToMany": { + "objectTypeRidOneSide": "ri.ot.store", + "objectTypeRidManySide": "ri.ot.sale", + "oneSidePrimaryKeyToManySidePropertyMapping": { + "ri.p.store_id": "ri.p.store_id" + } + } + } + } + ] +} diff --git a/converters/ontology/tests/fixtures/flights.yaml b/converters/ontology/tests/fixtures/specs/flights.yaml similarity index 100% rename from converters/ontology/tests/fixtures/flights.yaml rename to converters/ontology/tests/fixtures/specs/flights.yaml diff --git a/converters/ontology/tests/fixtures/specs/retail.yaml b/converters/ontology/tests/fixtures/specs/retail.yaml new file mode 100644 index 00000000..022c81b7 --- /dev/null +++ b/converters/ontology/tests/fixtures/specs/retail.yaml @@ -0,0 +1,1116 @@ +# 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. + +# Subtypes and derived formulas. Read by test_spec_snapshots and +# test_ossie_to_relationalai, and by test_reasoner for inherited reference +# schemes -- Contractor and PermanentEmployee extend Employee and are identified +# by its `nr`. CardPayment carries a concept-level `derived_by`, which is the +# subtype branch of the formula emitter. +version: 0.2.0.dev0 +name: RETAIL_REFACTORED +description: Retail ontology with employees, stores, products, transactions, orders, payments, and weekly sales facts. +ontology: +- concept: Amount + description: Amount of money exchanged in return for some item(s). + type: ValueType + extends: [Decimal] +- concept: EmployeeNr + description: Unique company-assigned employee number. + type: ValueType + extends: [Integer] + requires: [EmployeeNr > 0] +- concept: RegisterId + description: Unique identifier for point-of-sale terminals (cash registers). + type: ValueType + extends: [Integer] + requires: [RegisterId > 0] +- concept: StoreNr + description: Unique identifier for a physical store. + type: ValueType + extends: [Integer] +- concept: TransactionNr + description: Unique transaction number generated by a point-of-sale terminal. + type: ValueType + extends: [Integer] + requires: [TransactionNr > 0] +- concept: UniversalProductCode + description: 12-digit number used for tracking trade items. + type: ValueType + extends: [Integer] + requires: [0 < UniversalProductCode AND UniversalProductCode <= 999999999999] +- concept: ClassNr + description: Unique number generated for Class. + type: ValueType + extends: [Integer] +- concept: CategoryName + description: Unique name for a Category. + type: ValueType + extends: [String] +- concept: ClassLabelName + description: Unique name for a ClassLabel. + type: ValueType + extends: [String] +- concept: RegionName + description: Unique name for a Region. + type: ValueType + extends: [String] +- concept: OrderNr + description: Unique id for an Order. + type: ValueType + extends: [Integer] +- concept: PaymentId + description: Unique id for a Payment. + type: ValueType + extends: [Integer] +- concept: Employee + description: Employee of the company. + type: EntityType + identify_by: [nr] + relationships: + - name: nr + roles: + - concept: EmployeeNr + verbalizes: ['{Employee} is identified by {EmployeeNr}'] + multiplicity: OneToOne + - name: reportsTo + roles: + - concept: Employee + name: mgr + verbalizes: ['{Employee} reports to {Employee:mgr}'] + multiplicity: ManyToOne + - name: firstName + roles: + - concept: String + multiplicity: ManyToOne + - name: lastName + roles: + - concept: String + multiplicity: ManyToOne +- concept: Contractor + description: Employee with temporary contract. + type: EntityType + extends: [Employee] + relationships: + - name: hourlyRate + roles: + - concept: Integer + verbalizes: ['{Contractor} is paid hourly {Integer}'] + multiplicity: ManyToOne +- concept: PermanentEmployee + description: Employee with indefinite contract. + type: EntityType + extends: [Employee] + relationships: + - name: salary + roles: + - concept: Integer + verbalizes: ['{PermanentEmployee} has monthly salary {Integer}'] + multiplicity: ManyToOne +- concept: Item + description: An item for sale at a retailer. + type: EntityType + identify_by: [upc] + relationships: + - name: upc + roles: + - concept: UniversalProductCode + verbalizes: ['{Item} is identified by {UniversalProductCode}'] + multiplicity: OneToOne + - name: description + roles: + - concept: String + multiplicity: ManyToOne + - name: itemToClass + roles: + - concept: Class + verbalizes: ['{Item} is grouped by {Class}'] + multiplicity: ManyToOne + - name: weekly_sales + roles: + - concept: Store + - concept: Integer + - concept: Decimal + verbalizes: ['{Item} sold at {Store} in week {Integer} for {Decimal}'] + multiplicity: ManyToOne + - name: weekly_gross_sales + roles: + - concept: Store + - concept: Integer + - concept: Decimal + verbalizes: ['{Item} sold at {Store} in week {Integer} for gross {Decimal}'] + multiplicity: ManyToOne + - name: weekly_gross_returns + roles: + - concept: Store + - concept: Integer + - concept: Decimal + verbalizes: ['{Item} returned at {Store} in week {Integer} for {Decimal}'] + multiplicity: ManyToOne + - name: weekly_net_revenue + roles: + - concept: Store + - concept: Integer + - concept: Decimal + verbalizes: ['{Item} sold at {Store} in week {Integer} for net {Decimal}'] + multiplicity: ManyToOne + - name: item_store_sales + roles: + - concept: Store + - concept: Decimal + verbalizes: ['{Item} sold at {Store} for total {Decimal}'] + multiplicity: ManyToOne +- concept: Category + description: Category of the Product. + type: EntityType + identify_by: [name] + relationships: + - name: name + roles: + - concept: CategoryName + verbalizes: ['{Category} is identified by {CategoryName}'] + multiplicity: OneToOne +- concept: ClassLabel + description: ClassLabel of the Product. + type: EntityType + identify_by: [name] + relationships: + - name: name + roles: + - concept: ClassLabelName + verbalizes: ['{ClassLabel} is identified by {ClassLabelName}'] + multiplicity: OneToOne +- concept: Class + description: Class of the Product. + type: EntityType + identify_by: [nr] + relationships: + - name: nr + roles: + - concept: ClassNr + verbalizes: ['{Class} is identified by {ClassNr}'] + multiplicity: OneToOne + - name: classToCategory + roles: + - concept: Category + verbalizes: ['{Class} is part of {Category}'] + multiplicity: ManyToOne + - name: classToLabel + roles: + - concept: ClassLabel + verbalizes: ['{Class} has {ClassLabel}'] + multiplicity: OneToOne +- concept: Region + description: Region of a country. + type: EntityType + identify_by: [name] + relationships: + - name: name + roles: + - concept: RegionName + verbalizes: ['{Region} is identified by {RegionName}'] + multiplicity: OneToOne +- concept: Store + description: Physical store that offers items for sale to the public. + type: EntityType + identify_by: [nr] + relationships: + - name: nr + roles: + - concept: StoreNr + verbalizes: ['{Store} is identified by {StoreNr}'] + multiplicity: OneToOne + - name: storeToManager + roles: + - concept: Employee + verbalizes: ['{Store} is managed by {Employee}'] + multiplicity: ManyToOne + - name: storeToRegion + roles: + - concept: Region + verbalizes: ['{Store} belongs to {Region}'] + multiplicity: ManyToOne + - name: avgReturns + roles: + - concept: Item + - concept: Amount + name: amt + verbalizes: ['{Store} reports returns of {Item} for average {Amount:amt}'] + multiplicity: ManyToOne + derived_by: ['amt == AVG[Amount WHERE Transaction.registeredBy(Terminal) AND Transaction.returns(Transaction, Item, Amount) AND Terminal.deployedTo(Store) GROUP BY Item, Store]'] + - name: avgReturnsWithMarkup + roles: + - concept: Item + - concept: Amount + name: amt + verbalizes: ['{Store} reports returns of {Item} for 10%-marked-up average {Amount:amt}'] + multiplicity: ManyToOne + derived_by: ['amt == Amount(AVG[Amount WHERE Transaction.registeredBy(Terminal) AND Transaction.returns(Transaction, Item, Amount) AND Terminal.deployedTo(Store) GROUP BY Item, Store] * 1.1)'] +- concept: Terminal + description: A point-of-sale terminal for conducting retail transactions. + type: EntityType + identify_by: [id] + relationships: + - name: id + roles: + - concept: RegisterId + verbalizes: ['{Terminal} is identified by {RegisterId}'] + multiplicity: OneToOne + - name: deployedTo + roles: + - concept: Store + verbalizes: ['{Terminal} is deployed to {Store}'] + multiplicity: ManyToOne +- concept: Transaction + description: Transaction registered by a point-of-sale terminal. + type: EntityType + identify_by: [nr] + relationships: + - name: nr + roles: + - concept: TransactionNr + verbalizes: ['{Transaction} is identified by {TransactionNr}'] + multiplicity: OneToOne + - name: registeredBy + roles: + - concept: Terminal + verbalizes: ['{Transaction} is registered by {Terminal}'] + multiplicity: ManyToOne + - name: returns + roles: + - concept: Item + - concept: Amount + verbalizes: ['{Transaction} refunds {Item} for {Amount}'] + multiplicity: ManyToOne + - name: sales + roles: + - concept: Item + - concept: Amount + verbalizes: ['{Transaction} sells {Item} for {Amount}'] + multiplicity: ManyToOne + - name: transactionTerminalStore + roles: + - concept: Terminal + - concept: Store + verbalizes: ['{Transaction} registered at {Terminal} deployed to {Store}'] + derived_by: ['Transaction.registeredBy(Terminal) AND Terminal.deployedTo(Store)'] + - name: storeNrViaBinaryChain + roles: + - concept: StoreNr + name: store_nr + verbalizes: ['{Transaction} was registered at a terminal deployed to store {StoreNr:store_nr}'] + multiplicity: ManyToOne + # A binary fact continues the traversal from its last role: `registeredBy` + # lands on the Terminal, `deployedTo` on the Store, `nr` on its StoreNr. + derived_by: ['store_nr == Transaction.registeredBy(Terminal).deployedTo.nr'] + - name: storeNrViaTernaryChain + roles: + - concept: StoreNr + name: store_nr + verbalizes: ['{Transaction} occurred at store {StoreNr:store_nr}'] + multiplicity: ManyToOne + # The same, off a ternary applied to all but its last role: the dot join + # continues from the Store playing that role. Must agree with the binary + # chain above, and with `transactionTerminalStore` fully applied. + derived_by: ['store_nr == Transaction.transactionTerminalStore(Terminal).nr'] + - name: transactionRegion + roles: + - concept: Region + verbalizes: ['{Transaction} occurred in {Region}'] + multiplicity: ManyToOne + derived_by: ['Transaction.registeredBy(Terminal) AND Terminal.deployedTo(Store) AND Store.storeToRegion(Region)'] + - name: transactionStoreManager + roles: + - concept: Employee + verbalizes: ['{Transaction} associated with store manager {Employee}'] + multiplicity: ManyToOne + derived_by: ['Transaction.registeredBy(Terminal) AND Terminal.deployedTo(Store) AND Store.storeToManager(Employee)'] +- concept: Order + description: Order in a Store. + type: EntityType + identify_by: [nr] + relationships: + - name: nr + roles: + - concept: OrderNr + verbalizes: ['{Order} is identified by {OrderNr}'] + multiplicity: OneToOne + - name: orderDate + roles: + - concept: Date + verbalizes: ['{Order} has {Date}'] + multiplicity: ManyToOne +- concept: OrderLineItem + description: LineItem in an Order. + type: EntityType + identify_by: [order, nr] + relationships: + - name: order + roles: + - concept: Order + verbalizes: ['{OrderLineItem} belongs to {Order}'] + multiplicity: ManyToOne + - name: nr + roles: + - concept: Integer + verbalizes: ['{OrderLineItem} has line item number {Integer}'] + multiplicity: ManyToOne + - name: lineitem_extended_price + description: Extended price for the line item. + roles: + - concept: Decimal + name: lineitem_extended_price + verbalizes: ['{OrderLineItem} has extended price {Decimal:lineitem_extended_price}'] + multiplicity: ManyToOne + - name: lineitem_discount + description: Discount factor applied to the line. + roles: + - concept: Decimal + name: lineitem_discount + verbalizes: ['{OrderLineItem} has discount factor {Decimal:lineitem_discount}'] + multiplicity: ManyToOne + - name: net_revenue + description: The net extended price of the order line after discount, but before tax. + roles: + - concept: Decimal + name: net_revenue + verbalizes: ['{OrderLineItem} has net revenue {Decimal:net_revenue}'] + multiplicity: ManyToOne + derived_by: ['extended_price == OrderLineItem.lineitem_extended_price AND discount == OrderLineItem.lineitem_discount AND net_revenue == extended_price * (1 - discount)'] + - name: discounted_amount + description: The amount paid after subtracting the discount from the extended price. + roles: + - concept: Amount + name: discounted_amount + verbalizes: ['{OrderLineItem} has discounted amount {Amount:discounted_amount}'] + multiplicity: ManyToOne + derived_by: ['discounted_amount == Amount(OrderLineItem.lineitem_extended_price - OrderLineItem.lineitem_discount)'] +- concept: Payment + description: Order Payment. + type: EntityType + identify_by: [id] + relationships: + - name: id + roles: + - concept: PaymentId + verbalizes: ['{Payment} is identified by {PaymentId}'] + multiplicity: OneToOne + - name: type + roles: + - concept: String + multiplicity: ManyToOne + - name: amount + roles: + - concept: Decimal + multiplicity: ManyToOne +- concept: CardPayment + description: Payment made by card. + type: EntityType + extends: [Payment] + derived_by: ["Payment.type == 'card'"] + relationships: + - name: cardNumber + roles: + - concept: String + multiplicity: ManyToOne + - name: cardHolderName + roles: + - concept: String + multiplicity: ManyToOne +- concept: CashPayment + description: Payment made in cash. + type: EntityType + extends: [Payment] + derived_by: ["Payment.type == 'cash'"] + relationships: + - name: cashReceived + roles: + - concept: Decimal + multiplicity: ManyToOne + - name: change + roles: + - concept: Decimal + multiplicity: ManyToOne +ontology_mappings: +- name: retail_refactored_mapping + semantic_model: + name: Retail refactored semantic model + datasets: + - name: EMPLOYEES + source: DB.SCHEMA.EMPLOYEES + fields: + - name: empNr + expression: + dialects: + - dialect: ANSI_SQL + expression: empNr + - name: lastName + expression: + dialects: + - dialect: ANSI_SQL + expression: lastName + - name: firstName + expression: + dialects: + - dialect: ANSI_SQL + expression: firstName + - name: mgr + expression: + dialects: + - dialect: ANSI_SQL + expression: mgr + - name: addr + expression: + dialects: + - dialect: ANSI_SQL + expression: addr + - name: city + expression: + dialects: + - dialect: ANSI_SQL + expression: city + - name: state + expression: + dialects: + - dialect: ANSI_SQL + expression: state + - name: zip + expression: + dialects: + - dialect: ANSI_SQL + expression: zip + - name: CONTRACTORS + source: DB.SCHEMA.CONTRACTORS + fields: + - name: empNr + expression: + dialects: + - dialect: ANSI_SQL + expression: empNr + - name: hourlyRate + expression: + dialects: + - dialect: ANSI_SQL + expression: hourlyRate + - name: PERMANENT_EMPLOYEES + source: DB.SCHEMA.PERMANENT_EMPLOYEES + fields: + - name: empNr + expression: + dialects: + - dialect: ANSI_SQL + expression: empNr + - name: salary + expression: + dialects: + - dialect: ANSI_SQL + expression: salary + - name: LOCATIONS + source: DB.SCHEMA.LOCATIONS + fields: + - name: storeNr + expression: + dialects: + - dialect: ANSI_SQL + expression: storeNr + - name: mgr + expression: + dialects: + - dialect: ANSI_SQL + expression: mgr + - name: region + expression: + dialects: + - dialect: ANSI_SQL + expression: region + - name: addr + expression: + dialects: + - dialect: ANSI_SQL + expression: addr + - name: city + expression: + dialects: + - dialect: ANSI_SQL + expression: city + - name: state + expression: + dialects: + - dialect: ANSI_SQL + expression: state + - name: zip + expression: + dialects: + - dialect: ANSI_SQL + expression: zip + - name: PRODUCTS + source: DB.SCHEMA.PRODUCTS + fields: + - name: upc + expression: + dialects: + - dialect: ANSI_SQL + expression: upc + - name: description + expression: + dialects: + - dialect: ANSI_SQL + expression: description + - name: classNr + expression: + dialects: + - dialect: ANSI_SQL + expression: classNr + - name: classLabel + expression: + dialects: + - dialect: ANSI_SQL + expression: classLabel + - name: category + expression: + dialects: + - dialect: ANSI_SQL + expression: category + - name: TRANSACTIONS + source: DB.SCHEMA.TRANSACTIONS + fields: + - name: txn + expression: + dialects: + - dialect: ANSI_SQL + expression: txn + - name: sku + expression: + dialects: + - dialect: ANSI_SQL + expression: sku + - name: terminal + expression: + dialects: + - dialect: ANSI_SQL + expression: terminal + - name: amt + expression: + dialects: + - dialect: ANSI_SQL + expression: amt + - name: refundYN + expression: + dialects: + - dialect: ANSI_SQL + expression: refundYN + - name: timestamp + expression: + dialects: + - dialect: ANSI_SQL + expression: timestamp + - name: TERMINALS + source: DB.SCHEMA.TERMINALS + fields: + - name: id + expression: + dialects: + - dialect: ANSI_SQL + expression: id + - name: store + expression: + dialects: + - dialect: ANSI_SQL + expression: store + - name: svcDate + expression: + dialects: + - dialect: ANSI_SQL + expression: svcDate + - name: LINEITEMS + source: DB.SCHEMA.LINEITEMS + fields: + - name: quantity + expression: + dialects: + - dialect: ANSI_SQL + expression: quantity + - name: extendedPrice + expression: + dialects: + - dialect: ANSI_SQL + expression: extendedPrice + - name: discount + expression: + dialects: + - dialect: ANSI_SQL + expression: discount + - name: tax + expression: + dialects: + - dialect: ANSI_SQL + expression: tax + - name: orderNr + expression: + dialects: + - dialect: ANSI_SQL + expression: orderNr + - name: lineItemNr + expression: + dialects: + - dialect: ANSI_SQL + expression: lineItemNr + - name: upc + expression: + dialects: + - dialect: ANSI_SQL + expression: upc + - name: status + expression: + dialects: + - dialect: ANSI_SQL + expression: status + - name: shipMode + expression: + dialects: + - dialect: ANSI_SQL + expression: shipMode + - name: shipDate + expression: + dialects: + - dialect: ANSI_SQL + expression: shipDate + - name: receiptDate + expression: + dialects: + - dialect: ANSI_SQL + expression: receiptDate + - name: ORDERS + source: DB.SCHEMA.ORDERS + fields: + - name: nr + expression: + dialects: + - dialect: ANSI_SQL + expression: nr + - name: store + expression: + dialects: + - dialect: ANSI_SQL + expression: store + - name: ordDate + expression: + dialects: + - dialect: ANSI_SQL + expression: ordDate + - name: PAYMENTS + source: DB.SCHEMA.PAYMENTS + fields: + - name: id + expression: + dialects: + - dialect: ANSI_SQL + expression: id + - name: orderNr + expression: + dialects: + - dialect: ANSI_SQL + expression: orderNr + - name: paymentDate + expression: + dialects: + - dialect: ANSI_SQL + expression: paymentDate + - name: amount + expression: + dialects: + - dialect: ANSI_SQL + expression: amount + - name: type + expression: + dialects: + - dialect: ANSI_SQL + expression: type + - name: cardNumber + expression: + dialects: + - dialect: ANSI_SQL + expression: cardNumber + - name: cardExpiry + expression: + dialects: + - dialect: ANSI_SQL + expression: cardExpiry + - name: cardHolderName + expression: + dialects: + - dialect: ANSI_SQL + expression: cardHolderName + - name: cashReceived + expression: + dialects: + - dialect: ANSI_SQL + expression: cashReceived + - name: RETAIL_FACTS + source: DB.SCHEMA.RETAIL_FACTS + fields: + - name: item_nr + expression: + dialects: + - dialect: ANSI_SQL + expression: item_nr + - name: store_nr + expression: + dialects: + - dialect: ANSI_SQL + expression: store_nr + - name: week_nr + expression: + dialects: + - dialect: ANSI_SQL + expression: week_nr + - name: sls + expression: + dialects: + - dialect: ANSI_SQL + expression: sls + - name: returns + expression: + dialects: + - dialect: ANSI_SQL + expression: returns + concept_mappings: + - concept: Employee + object_mappings: + - referent_mappings: + - relationship: nr + expression: EMPLOYEES.empNr + link_mappings: + - object_mapping: + referent_mappings: + - relationship: nr + expression: EMPLOYEES.empNr + children: + - object_mapping: + concept: Employee + referent_mappings: + - relationship: nr + expression: EMPLOYEES.mgr + relationship: reportsTo + - object_mapping: + concept: String + expression: EMPLOYEES.firstName + relationship: firstName + - object_mapping: + concept: String + expression: EMPLOYEES.lastName + relationship: lastName + - concept: Contractor + object_mappings: + - concept: Employee + referent_mappings: + - relationship: nr + expression: CONTRACTORS.empNr + link_mappings: + - object_mapping: + concept: Employee + referent_mappings: + - relationship: nr + expression: CONTRACTORS.empNr + children: + - object_mapping: + concept: Integer + expression: CONTRACTORS.hourlyRate + relationship: hourlyRate + - concept: PermanentEmployee + object_mappings: + - concept: Employee + referent_mappings: + - relationship: nr + expression: PERMANENT_EMPLOYEES.empNr + link_mappings: + - object_mapping: + concept: Employee + referent_mappings: + - relationship: nr + expression: PERMANENT_EMPLOYEES.empNr + children: + - object_mapping: + concept: Integer + expression: PERMANENT_EMPLOYEES.salary + relationship: salary + - concept: Item + object_mappings: + - referent_mappings: + - relationship: upc + expression: PRODUCTS.upc + link_mappings: + - object_mapping: + referent_mappings: + - relationship: upc + expression: PRODUCTS.upc + children: + - object_mapping: + concept: Class + referent_mappings: + - relationship: nr + expression: PRODUCTS.classNr + relationship: itemToClass + - object_mapping: + concept: String + expression: PRODUCTS.description + relationship: description + - object_mapping: + referent_mappings: + - relationship: upc + expression: RETAIL_FACTS.item_nr + children: + - object_mapping: + concept: Store + referent_mappings: + - relationship: nr + expression: RETAIL_FACTS.store_nr + children: + - object_mapping: + concept: Integer + expression: RETAIL_FACTS.week_nr + children: + - object_mapping: + concept: Decimal + expression: RETAIL_FACTS.sls + relationship: weekly_sales + - object_mapping: + concept: Decimal + expression: RETAIL_FACTS.sls + RETAIL_FACTS.returns + relationship: weekly_gross_sales + - object_mapping: + concept: Decimal + expression: RETAIL_FACTS.returns + relationship: weekly_gross_returns + - object_mapping: + concept: Decimal + expression: RETAIL_FACTS.sls * 0.9 - RETAIL_FACTS.returns * 1.1 + 100 + relationship: weekly_net_revenue + - object_mapping: + referent_mappings: + - relationship: upc + expression: RETAIL_FACTS.item_nr + children: + - object_mapping: + concept: Store + referent_mappings: + - relationship: nr + expression: RETAIL_FACTS.store_nr + children: + - object_mapping: + concept: Decimal + expression: SUM[RETAIL_FACTS.sls WHERE RETAIL_FACTS.sls > 0 GROUP BY RETAIL_FACTS.item_nr, RETAIL_FACTS.store_nr] + relationship: item_store_sales + - concept: Category + object_mappings: + - referent_mappings: + - relationship: name + expression: PRODUCTS.category + - concept: ClassLabel + object_mappings: + - referent_mappings: + - relationship: name + expression: PRODUCTS.classLabel + - concept: Class + object_mappings: + - referent_mappings: + - relationship: nr + expression: PRODUCTS.classNr + link_mappings: + - object_mapping: + referent_mappings: + - relationship: nr + expression: PRODUCTS.classNr + children: + - object_mapping: + concept: Category + referent_mappings: + - relationship: name + expression: PRODUCTS.category + relationship: classToCategory + - object_mapping: + concept: ClassLabel + referent_mappings: + - relationship: name + expression: PRODUCTS.classLabel + relationship: classToLabel + - concept: Region + object_mappings: + - referent_mappings: + - relationship: name + expression: LOCATIONS.region + - concept: Store + object_mappings: + - referent_mappings: + - relationship: nr + expression: LOCATIONS.storeNr + link_mappings: + - object_mapping: + referent_mappings: + - relationship: nr + expression: LOCATIONS.storeNr + children: + - object_mapping: + concept: Employee + referent_mappings: + - relationship: nr + expression: LOCATIONS.mgr + relationship: storeToManager + - object_mapping: + concept: Region + referent_mappings: + - relationship: name + expression: LOCATIONS.region + relationship: storeToRegion + - concept: Terminal + object_mappings: + - referent_mappings: + - relationship: id + expression: TERMINALS.id + link_mappings: + - object_mapping: + referent_mappings: + - relationship: id + expression: TERMINALS.id + children: + - object_mapping: + concept: Store + referent_mappings: + - relationship: nr + expression: TERMINALS.store + relationship: deployedTo + - concept: Transaction + object_mappings: + - referent_mappings: + - relationship: nr + expression: TRANSACTIONS.txn + link_mappings: + - object_mapping: + referent_mappings: + - relationship: nr + expression: TRANSACTIONS.txn + children: + - object_mapping: + concept: Terminal + referent_mappings: + - relationship: id + expression: TRANSACTIONS.terminal + relationship: registeredBy + - object_mapping: + concept: Item + referent_mappings: + - relationship: upc + expression: TRANSACTIONS.sku + children: + - object_mapping: + concept: Amount + expression: TRANSACTIONS.amt WHERE (TRANSACTIONS.refundYN == 'Y') + relationship: returns + - object_mapping: + concept: Amount + expression: TRANSACTIONS.amt WHERE (NOT TRANSACTIONS.refundYN == 'Y') + relationship: sales + - concept: Order + object_mappings: + - referent_mappings: + - relationship: nr + expression: ORDERS.nr + link_mappings: + - object_mapping: + referent_mappings: + - relationship: nr + expression: ORDERS.nr + children: + - object_mapping: + concept: Date + expression: ORDERS.ordDate + relationship: orderDate + - concept: OrderLineItem + object_mappings: + - referent_mappings: + - relationship: order + referent_mappings: + - relationship: nr + expression: LINEITEMS.orderNr + - relationship: nr + expression: LINEITEMS.lineItemNr + link_mappings: + - object_mapping: + referent_mappings: + - relationship: order + referent_mappings: + - relationship: nr + expression: LINEITEMS.orderNr + - relationship: nr + expression: LINEITEMS.lineItemNr + children: + - object_mapping: + concept: Decimal + expression: LINEITEMS.extendedPrice + relationship: lineitem_extended_price + - object_mapping: + concept: Decimal + expression: LINEITEMS.discount + relationship: lineitem_discount + - concept: Payment + object_mappings: + - referent_mappings: + - relationship: id + expression: PAYMENTS.id + link_mappings: + - object_mapping: + referent_mappings: + - relationship: id + expression: PAYMENTS.id + children: + - object_mapping: + concept: String + expression: PAYMENTS.type + relationship: type + - object_mapping: + concept: Decimal + expression: PAYMENTS.amount + relationship: amount + - concept: CardPayment + link_mappings: + - object_mapping: + referent_mappings: + - relationship: id + expression: PAYMENTS.id WHERE (PAYMENTS.type == 'card') + children: + - object_mapping: + concept: String + expression: PAYMENTS.cardNumber + relationship: cardNumber + - object_mapping: + concept: String + expression: PAYMENTS.cardHolderName + relationship: cardHolderName + - concept: CashPayment + link_mappings: + - object_mapping: + referent_mappings: + - relationship: id + expression: PAYMENTS.id WHERE (PAYMENTS.type == 'cash') + children: + - object_mapping: + concept: Decimal + expression: PAYMENTS.cashReceived + relationship: cashReceived + - object_mapping: + concept: Decimal + expression: PAYMENTS.cashReceived - PAYMENTS.amount + relationship: change \ No newline at end of file diff --git a/converters/ontology/tests/fixtures/specs/tpch.yaml b/converters/ontology/tests/fixtures/specs/tpch.yaml new file mode 100644 index 00000000..75a1756e --- /dev/null +++ b/converters/ontology/tests/fixtures/specs/tpch.yaml @@ -0,0 +1,1211 @@ +# 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. + +# Breadth: the largest corpus spec, with window-function metrics and a wide +# relationship graph. Read by test_spec_snapshots and test_ossie_to_relationalai +# only -- it earns its place by rendering a lot of shapes at once, not by +# demonstrating any single behaviour. +version: 0.2.0.dev0 +name: snow_tpch +description: TPC-H ontology with customers, orders, line items, parts, suppliers, nations, and regions, mapped to the KSE.TPCH_SF1 Snowflake schema. +ontology: +- concept: Region + description: 'The regions table contains details about various geographic regions, + each uniquely identified by a region key. Each region can have multiple nations + associated with it. There are a total of 5 regions.' + type: EntityType + identify_by: [region_key] + relationships: + - name: region_key + roles: + - concept: Integer + verbalizes: ['{Region} is identified by {Integer}'] + multiplicity: OneToOne + - name: region_comment + description: Free-form region comment + roles: + - concept: String + name: region_comment + verbalizes: ['{Region} has {String:region_comment}'] + multiplicity: ManyToOne + - name: region_name + description: Geographic region of the world. Name of the region. + roles: + - concept: String + name: region_name + verbalizes: ['{Region} has {String:region_name}'] + multiplicity: ManyToOne +- concept: Nation + description: 'The nations table contains details about various countries, including + a unique identifier for each nation, its name, and a reference to the region + it belongs to. Each nation is uniquely identified by N_NATIONKEY.' + type: EntityType + identify_by: [nation_key] + relationships: + - name: nation_key + roles: + - concept: Integer + verbalizes: ['{Nation} is identified by {Integer}'] + multiplicity: OneToOne + - name: nation_comment + description: Free-form nation comment + roles: + - concept: String + name: nation_comment + verbalizes: ['{Nation} has {String:nation_comment}'] + multiplicity: ManyToOne + - name: region_key + description: Foreign key from the regions table + roles: + - concept: Integer + name: region_key + verbalizes: ['{Nation} has {Integer:region_key}'] + multiplicity: ManyToOne + - name: nation_name + description: List of nations/countries + roles: + - concept: String + name: nation_name + verbalizes: ['{Nation} has {String:nation_name}'] + multiplicity: ManyToOne + - name: North_America + description: A filter to restrict data to only North American Continent + roles: [] + verbalizes: ['{Nation} has North America filter'] + - name: nations_to_regions + description: A relationship that maps instances of Nation to instances of Region + roles: + - concept: Region + verbalizes: ['{Nation} maps to {Region}'] + multiplicity: ManyToOne + derived_by: ['Nation.region_key == Region.region_key'] +- concept: Supplier + description: 'The suppliers table contains details about suppliers, including + unique supplier keys, supplier names, addresses, and associated nations.' + type: EntityType + identify_by: [supplier_key] + relationships: + - name: supplier_key + roles: + - concept: Integer + verbalizes: ['{Supplier} is identified by {Integer}'] + multiplicity: OneToOne + - name: supplier_address + description: Supplier address + roles: + - concept: String + name: supplier_address + verbalizes: ['{Supplier} has {String:supplier_address}'] + multiplicity: ManyToOne + - name: supplier_phone + description: Supplier phone number + roles: + - concept: String + name: supplier_phone + verbalizes: ['{Supplier} has {String:supplier_phone}'] + multiplicity: ManyToOne + - name: supplier_account_balance + description: Supplier account balance + roles: + - concept: Decimal + name: supplier_account_balance + verbalizes: ['{Supplier} has {Decimal:supplier_account_balance}'] + multiplicity: ManyToOne + - name: supplier_comment + description: Free-form supplier comment + roles: + - concept: String + name: supplier_comment + verbalizes: ['{Supplier} has {String:supplier_comment}'] + multiplicity: ManyToOne + - name: supplier_name + description: Name of the supplier + roles: + - concept: String + name: supplier_name + verbalizes: ['{Supplier} has {String:supplier_name}'] + multiplicity: ManyToOne + - name: nation_key + description: Foreign key from the nations table + roles: + - concept: Integer + name: nation_key + verbalizes: ['{Supplier} has {Integer:nation_key}'] + multiplicity: ManyToOne + - name: suppliers_to_nations + description: A relationship that maps instances of Supplier to instances of Nation + roles: + - concept: Nation + verbalizes: ['{Supplier} maps to {Nation}'] + multiplicity: ManyToOne + derived_by: ['Supplier.nation_key == Nation.nation_key'] +- concept: Part + description: "The parts table contains details about parts that suppliers offer, + including unique part keys, names, and types. A part could be supplied from + multple suppliers." + type: EntityType + identify_by: [part_key] + relationships: + - name: part_key + roles: + - concept: Integer + verbalizes: ['{Part} is identified by {Integer}'] + multiplicity: OneToOne + - name: part_retail_price + description: Part retail price + roles: + - concept: Decimal + name: part_retail_price + verbalizes: ['{Part} has {Decimal:part_retail_price}'] + multiplicity: ManyToOne + - name: part_comment + description: Free-form part comment + roles: + - concept: String + name: part_comment + verbalizes: ['{Part} has {String:part_comment}'] + multiplicity: ManyToOne + - name: part_name + description: name of the parts + roles: + - concept: String + name: part_name + verbalizes: ['{Part} has {String:part_name}'] + multiplicity: ManyToOne + - name: part_manufacturer_name + description: Manufacture name of this part + roles: + - concept: String + name: part_manufacturer_name + verbalizes: ['{Part} has {String:part_manufacturer_name}'] + multiplicity: ManyToOne + - name: part_brand + description: Brand Names of the part + roles: + - concept: String + name: part_brand + verbalizes: ['{Part} has {String:part_brand}'] + multiplicity: ManyToOne + - name: part_type + description: type of the part + roles: + - concept: String + name: part_type + verbalizes: ['{Part} has {String:part_type}'] + multiplicity: ManyToOne + - name: part_container + description: Container Types for each part + roles: + - concept: String + name: part_container + verbalizes: ['{Part} has {String:part_container}'] + multiplicity: ManyToOne + - name: part_size + description: Size of the part + roles: + - concept: Integer + name: part_size + verbalizes: ['{Part} has {Integer:part_size}'] + multiplicity: ManyToOne +- concept: PartSupplier + description: 'The partsupp table contains information linking parts to suppliers, + including part keys, supplier keys, and supply costs.' + type: EntityType + identify_by: [part_key, supplier_key] + relationships: + - name: part_key + roles: + - concept: Integer + verbalizes: ['{PartSupplier} is identified by part {Integer}'] + multiplicity: ManyToOne + - name: supplier_key + roles: + - concept: Integer + verbalizes: ['{PartSupplier} is identified by supplier {Integer}'] + multiplicity: ManyToOne + - name: available_quantity + description: Available quantity for this part-supplier pair + roles: + - concept: Decimal + name: available_quantity + verbalizes: ['{PartSupplier} has {Decimal:available_quantity}'] + multiplicity: ManyToOne + - name: part_supplier_supply_cost + description: Supply cost for this part-supplier pair + roles: + - concept: Decimal + name: part_supplier_supply_cost + verbalizes: ['{PartSupplier} has {Decimal:part_supplier_supply_cost}'] + multiplicity: ManyToOne + - name: part_supplier_comment + description: Free-form comment on the part-supplier record + roles: + - concept: String + name: part_supplier_comment + verbalizes: ['{PartSupplier} has {String:part_supplier_comment}'] + multiplicity: ManyToOne + - name: part_suppliers_to_part + description: A relationship that maps instances of PartSupplier to instances of Part + roles: + - concept: Part + verbalizes: ['{PartSupplier} maps to {Part}'] + multiplicity: ManyToOne + derived_by: ['PartSupplier.part_key == Part.part_key'] + - name: part_suppliers_to_supplier + description: A relationship that maps instances of PartSupplier to instances of Supplier + roles: + - concept: Supplier + verbalizes: ['{PartSupplier} maps to {Supplier}'] + multiplicity: ManyToOne + derived_by: ['PartSupplier.supplier_key == Supplier.supplier_key'] +- concept: CustOrder + description: "The orders table contains information about customer orders, including + the order date, priority, and status." + type: EntityType + identify_by: [order_key] + relationships: + - name: order_key + roles: + - concept: Integer + verbalizes: ['{CustOrder} is identified by {Integer}'] + multiplicity: OneToOne + - name: order_gross_total + description: Order total price + roles: + - concept: Decimal + name: order_gross_total + verbalizes: ['{CustOrder} has {Decimal:order_gross_total}'] + multiplicity: ManyToOne + - name: order_clerk + description: Clerk handling the order + roles: + - concept: String + name: order_clerk + verbalizes: ['{CustOrder} has {String:order_clerk}'] + multiplicity: ManyToOne + - name: order_ship_priority + description: Shipping priority for the order + roles: + - concept: Decimal + name: order_ship_priority + verbalizes: ['{CustOrder} has {Decimal:order_ship_priority}'] + multiplicity: ManyToOne + - name: order_comment + description: Free-form order comment + roles: + - concept: String + name: order_comment + verbalizes: ['{CustOrder} has {String:order_comment}'] + multiplicity: ManyToOne + - name: customer_key + description: Foreign key from Customer Table + roles: + - concept: Integer + name: customer_key + verbalizes: ['{CustOrder} has {Integer:customer_key}'] + multiplicity: ManyToOne + - name: order_status + description: Order Status + roles: + - concept: String + name: order_status + verbalizes: ['{CustOrder} has {String:order_status}'] + multiplicity: ManyToOne + - name: order_priority + description: Priority level assigned to an order + roles: + - concept: String + name: order_priority + verbalizes: ['{CustOrder} has {String:order_priority}'] + multiplicity: ManyToOne + - name: order_date + description: Date of Order + roles: + - concept: Date + name: order_date + verbalizes: ['{CustOrder} has {Date:order_date}'] + multiplicity: ManyToOne + - name: Yesterday + description: A filter to restrict data to only yesterday. + roles: [] + verbalizes: ['{CustOrder} has Yesterday filter'] + - name: YTD + description: A filter to restrict data to only current fiscal year. + roles: [] + verbalizes: ['{CustOrder} has YTD filter'] + - name: orders_to_customers + description: A relationship that maps instances of CustOrder to instances of Customer + roles: + - concept: Customer + verbalizes: ['{CustOrder} maps to {Customer}'] + multiplicity: ManyToOne + derived_by: ['CustOrder.customer_key == Customer.customer_key'] +- concept: OrderLineitem + description: 'The lineitems table contains details of individual items within + orders, including part numbers, quantities, and discounts.' + type: EntityType + identify_by: [order_key, order_lineitem_number] + relationships: + - name: order_key + roles: + - concept: Integer + verbalizes: ['{OrderLineitem} is identified by order {Integer}'] + multiplicity: ManyToOne + - name: order_lineitem_number + roles: + - concept: Integer + verbalizes: ['{OrderLineitem} is identified by line {Integer}'] + multiplicity: ManyToOne + - name: lineitem_quantity + description: Quantity ordered + roles: + - concept: Decimal + name: lineitem_quantity + verbalizes: ['{OrderLineitem} has {Decimal:lineitem_quantity}'] + multiplicity: ManyToOne + - name: lineitem_extended_price + description: Extended price for the line item + roles: + - concept: Decimal + name: lineitem_extended_price + verbalizes: ['{OrderLineitem} has {Decimal:lineitem_extended_price}'] + multiplicity: ManyToOne + - name: lineitem_discount + description: Discount factor applied to the line + roles: + - concept: Decimal + name: lineitem_discount + verbalizes: ['{OrderLineitem} has {Decimal:lineitem_discount}'] + multiplicity: ManyToOne + - name: lineitem_tax + description: Tax factor applied to the line + roles: + - concept: Decimal + name: lineitem_tax + verbalizes: ['{OrderLineitem} has {Decimal:lineitem_tax}'] + multiplicity: ManyToOne + - name: lineitem_comment + description: Free-form comment for the line + roles: + - concept: String + name: lineitem_comment + verbalizes: ['{OrderLineitem} has {String:lineitem_comment}'] + multiplicity: ManyToOne + - name: net_revenue + description: The net extended price of the order line after discount, but before tax. + roles: + - concept: Decimal + name: net_revenue + verbalizes: ['{OrderLineitem} has {Decimal:net_revenue}'] + multiplicity: ManyToOne + - name: gross_revenue + description: The net extended price of the order line after discount, but before tax. + roles: + - concept: Decimal + name: gross_revenue + verbalizes: ['{OrderLineitem} has {Decimal:gross_revenue}'] + multiplicity: ManyToOne + - name: part_key + description: Part Key + roles: + - concept: Integer + name: part_key + verbalizes: ['{OrderLineitem} has {Integer:part_key}'] + multiplicity: ManyToOne + - name: supplier_key + description: Supplier Key + roles: + - concept: Integer + name: supplier_key + verbalizes: ['{OrderLineitem} has {Integer:supplier_key}'] + multiplicity: ManyToOne + - name: lineitem_return_flag + description: Return Flag for Line Item + roles: + - concept: String + name: lineitem_return_flag + verbalizes: ['{OrderLineitem} has {String:lineitem_return_flag}'] + multiplicity: ManyToOne + - name: lineitem_status + description: Order Line Item Fulfillment Status + roles: + - concept: String + name: lineitem_status + verbalizes: ['{OrderLineitem} has {String:lineitem_status}'] + multiplicity: ManyToOne + - name: lineitem_shipping_instructions + description: Shipping Instructions + roles: + - concept: String + name: lineitem_shipping_instructions + verbalizes: ['{OrderLineitem} has {String:lineitem_shipping_instructions}'] + multiplicity: ManyToOne + - name: lineitem_shipping_mode + description: Order Lineitem Shipping Mode + roles: + - concept: String + name: lineitem_shipping_mode + verbalizes: ['{OrderLineitem} has {String:lineitem_shipping_mode}'] + multiplicity: ManyToOne + - name: lineitem_ship_date + description: Order line item Date of shipment + roles: + - concept: Date + name: lineitem_ship_date + verbalizes: ['{OrderLineitem} has {Date:lineitem_ship_date}'] + multiplicity: ManyToOne + - name: lineitem_commit_date + description: Order lineitem Date of Commitment + roles: + - concept: Date + name: lineitem_commit_date + verbalizes: ['{OrderLineitem} has {Date:lineitem_commit_date}'] + multiplicity: ManyToOne + - name: lineitem_receipt_date + description: Date of receipt for each order line item + roles: + - concept: Date + name: lineitem_receipt_date + verbalizes: ['{OrderLineitem} has {Date:lineitem_receipt_date}'] + multiplicity: ManyToOne + - name: lineitems_to_orders + description: A relationship that maps instances of OrderLineitem to instances of CustOrder + roles: + - concept: CustOrder + verbalizes: ['{OrderLineitem} maps to {CustOrder}'] + multiplicity: ManyToOne + derived_by: ['OrderLineitem.order_key == CustOrder.order_key'] + - name: order_lineitems_to_part_suppliers + description: A relationship that maps instances of OrderLineitem to instances of PartSupplier + roles: + - concept: PartSupplier + verbalizes: ['{OrderLineitem} maps to {PartSupplier}'] + multiplicity: ManyToOne + derived_by: ['OrderLineitem.part_key == PartSupplier.part_key AND OrderLineitem.supplier_key == PartSupplier.supplier_key'] +- concept: Customer + description: 'The customers table contains details about individual customers, + including their unique customer identifier, name, address, account balance, + and market segment. It is used to analyze customer demographics, purchasing + behavior, and segmentation. Join this table with orders and lineitem tables + to track sales performance and customer order history.' + type: EntityType + identify_by: [customer_key] + relationships: + - name: customer_key + roles: + - concept: Integer + verbalizes: ['{Customer} is identified by {Integer}'] + multiplicity: OneToOne + - name: customer_address + description: Customer address + roles: + - concept: String + name: customer_address + verbalizes: ['{Customer} has {String:customer_address}'] + multiplicity: ManyToOne + - name: customer_phone + description: Customer phone number + roles: + - concept: String + name: customer_phone + verbalizes: ['{Customer} has {String:customer_phone}'] + multiplicity: ManyToOne + - name: customer_account_balance + description: Customer account balance + roles: + - concept: Decimal + name: customer_account_balance + verbalizes: ['{Customer} has {Decimal:customer_account_balance}'] + multiplicity: ManyToOne + - name: customer_comment + description: Free-form customer comment + roles: + - concept: String + name: customer_comment + verbalizes: ['{Customer} has {String:customer_comment}'] + multiplicity: ManyToOne + - name: nation_key + description: Foreign key from the nations table + roles: + - concept: Integer + name: nation_key + verbalizes: ['{Customer} has {Integer:nation_key}'] + multiplicity: ManyToOne + - name: customer_name + description: Name of the customer. + roles: + - concept: String + name: customer_name + verbalizes: ['{Customer} has {String:customer_name}'] + multiplicity: ManyToOne + - name: customer_market_segment + description: The market segment for this customer + roles: + - concept: String + name: customer_market_segment + verbalizes: ['{Customer} has {String:customer_market_segment}'] + multiplicity: ManyToOne + - name: automobile_market_segment + description: A filter to retrieve customers in the AUTOMOBILE market segment. + roles: [] + verbalizes: ['{Customer} has automobile_market_segment filter'] + - name: top_10_customers_by_higest_account_balance + description: A filter to retrieve the top 10 customers with the highest account balance. + roles: [] + verbalizes: ['{Customer} has top_10_customers_by_higest_account_balance filter'] + - name: customers_to_nations + description: A relationship that maps instances of Customer to instances of Nation + roles: + - concept: Nation + verbalizes: ['{Customer} maps to {Nation}'] + multiplicity: ManyToOne + derived_by: ['Customer.nation_key == Nation.nation_key'] +ontology_mappings: +- name: snow_tpch_mapping + semantic_model: + name: TPC-H Snowflake semantic model + datasets: + - name: KSE_TPCH_SF1_CUSTOMER + source: KSE.TPCH_SF1.CUSTOMER + fields: + - name: C_CUSTKEY + expression: + dialects: + - dialect: ANSI_SQL + expression: C_CUSTKEY + - name: C_NAME + expression: + dialects: + - dialect: ANSI_SQL + expression: C_NAME + - name: C_ADDRESS + expression: + dialects: + - dialect: ANSI_SQL + expression: C_ADDRESS + - name: C_NATIONKEY + expression: + dialects: + - dialect: ANSI_SQL + expression: C_NATIONKEY + - name: C_PHONE + expression: + dialects: + - dialect: ANSI_SQL + expression: C_PHONE + - name: C_ACCTBAL + expression: + dialects: + - dialect: ANSI_SQL + expression: C_ACCTBAL + - name: C_MKTSEGMENT + expression: + dialects: + - dialect: ANSI_SQL + expression: C_MKTSEGMENT + - name: C_COMMENT + expression: + dialects: + - dialect: ANSI_SQL + expression: C_COMMENT + - name: KSE_TPCH_SF1_ORDERS + source: KSE.TPCH_SF1.ORDERS + fields: + - name: O_ORDERKEY + expression: + dialects: + - dialect: ANSI_SQL + expression: O_ORDERKEY + - name: O_CUSTKEY + expression: + dialects: + - dialect: ANSI_SQL + expression: O_CUSTKEY + - name: O_ORDERSTATUS + expression: + dialects: + - dialect: ANSI_SQL + expression: O_ORDERSTATUS + - name: O_TOTALPRICE + expression: + dialects: + - dialect: ANSI_SQL + expression: O_TOTALPRICE + - name: O_ORDERDATE + expression: + dialects: + - dialect: ANSI_SQL + expression: O_ORDERDATE + - name: O_ORDERPRIORITY + expression: + dialects: + - dialect: ANSI_SQL + expression: O_ORDERPRIORITY + - name: O_CLERK + expression: + dialects: + - dialect: ANSI_SQL + expression: O_CLERK + - name: O_SHIPPRIORITY + expression: + dialects: + - dialect: ANSI_SQL + expression: O_SHIPPRIORITY + - name: O_COMMENT + expression: + dialects: + - dialect: ANSI_SQL + expression: O_COMMENT + - name: KSE_TPCH_SF1_LINEITEM + source: KSE.TPCH_SF1.LINEITEM + fields: + - name: L_ORDERKEY + expression: + dialects: + - dialect: ANSI_SQL + expression: L_ORDERKEY + - name: L_PARTKEY + expression: + dialects: + - dialect: ANSI_SQL + expression: L_PARTKEY + - name: L_SUPPKEY + expression: + dialects: + - dialect: ANSI_SQL + expression: L_SUPPKEY + - name: L_LINENUMBER + expression: + dialects: + - dialect: ANSI_SQL + expression: L_LINENUMBER + - name: L_QUANTITY + expression: + dialects: + - dialect: ANSI_SQL + expression: L_QUANTITY + - name: L_EXTENDEDPRICE + expression: + dialects: + - dialect: ANSI_SQL + expression: L_EXTENDEDPRICE + - name: L_DISCOUNT + expression: + dialects: + - dialect: ANSI_SQL + expression: L_DISCOUNT + - name: L_TAX + expression: + dialects: + - dialect: ANSI_SQL + expression: L_TAX + - name: L_RETURNFLAG + expression: + dialects: + - dialect: ANSI_SQL + expression: L_RETURNFLAG + - name: L_LINESTATUS + expression: + dialects: + - dialect: ANSI_SQL + expression: L_LINESTATUS + - name: L_SHIPDATE + expression: + dialects: + - dialect: ANSI_SQL + expression: L_SHIPDATE + - name: L_COMMITDATE + expression: + dialects: + - dialect: ANSI_SQL + expression: L_COMMITDATE + - name: L_RECEIPTDATE + expression: + dialects: + - dialect: ANSI_SQL + expression: L_RECEIPTDATE + - name: L_SHIPINSTRUCT + expression: + dialects: + - dialect: ANSI_SQL + expression: L_SHIPINSTRUCT + - name: L_SHIPMODE + expression: + dialects: + - dialect: ANSI_SQL + expression: L_SHIPMODE + - name: L_COMMENT + expression: + dialects: + - dialect: ANSI_SQL + expression: L_COMMENT + - name: KSE_TPCH_SF1_PARTSUPP + source: KSE.TPCH_SF1.PARTSUPP + fields: + - name: PS_PARTKEY + expression: + dialects: + - dialect: ANSI_SQL + expression: PS_PARTKEY + - name: PS_SUPPKEY + expression: + dialects: + - dialect: ANSI_SQL + expression: PS_SUPPKEY + - name: PS_AVAILQTY + expression: + dialects: + - dialect: ANSI_SQL + expression: PS_AVAILQTY + - name: PS_SUPPLYCOST + expression: + dialects: + - dialect: ANSI_SQL + expression: PS_SUPPLYCOST + - name: PS_COMMENT + expression: + dialects: + - dialect: ANSI_SQL + expression: PS_COMMENT + - name: KSE_TPCH_SF1_PART + source: KSE.TPCH_SF1.PART + fields: + - name: P_PARTKEY + expression: + dialects: + - dialect: ANSI_SQL + expression: P_PARTKEY + - name: P_NAME + expression: + dialects: + - dialect: ANSI_SQL + expression: P_NAME + - name: P_MFGR + expression: + dialects: + - dialect: ANSI_SQL + expression: P_MFGR + - name: P_BRAND + expression: + dialects: + - dialect: ANSI_SQL + expression: P_BRAND + - name: P_TYPE + expression: + dialects: + - dialect: ANSI_SQL + expression: P_TYPE + - name: P_SIZE + expression: + dialects: + - dialect: ANSI_SQL + expression: P_SIZE + - name: P_CONTAINER + expression: + dialects: + - dialect: ANSI_SQL + expression: P_CONTAINER + - name: P_RETAILPRICE + expression: + dialects: + - dialect: ANSI_SQL + expression: P_RETAILPRICE + - name: P_COMMENT + expression: + dialects: + - dialect: ANSI_SQL + expression: P_COMMENT + - name: KSE_TPCH_SF1_SUPPLIER + source: KSE.TPCH_SF1.SUPPLIER + fields: + - name: S_SUPPKEY + expression: + dialects: + - dialect: ANSI_SQL + expression: S_SUPPKEY + - name: S_NAME + expression: + dialects: + - dialect: ANSI_SQL + expression: S_NAME + - name: S_ADDRESS + expression: + dialects: + - dialect: ANSI_SQL + expression: S_ADDRESS + - name: S_NATIONKEY + expression: + dialects: + - dialect: ANSI_SQL + expression: S_NATIONKEY + - name: S_PHONE + expression: + dialects: + - dialect: ANSI_SQL + expression: S_PHONE + - name: S_ACCTBAL + expression: + dialects: + - dialect: ANSI_SQL + expression: S_ACCTBAL + - name: S_COMMENT + expression: + dialects: + - dialect: ANSI_SQL + expression: S_COMMENT + - name: KSE_TPCH_SF1_NATION + source: KSE.TPCH_SF1.NATION + fields: + - name: N_NATIONKEY + expression: + dialects: + - dialect: ANSI_SQL + expression: N_NATIONKEY + - name: N_NAME + expression: + dialects: + - dialect: ANSI_SQL + expression: N_NAME + - name: N_REGIONKEY + expression: + dialects: + - dialect: ANSI_SQL + expression: N_REGIONKEY + - name: N_COMMENT + expression: + dialects: + - dialect: ANSI_SQL + expression: N_COMMENT + - name: KSE_TPCH_SF1_REGION + source: KSE.TPCH_SF1.REGION + fields: + - name: R_REGIONKEY + expression: + dialects: + - dialect: ANSI_SQL + expression: R_REGIONKEY + - name: R_NAME + expression: + dialects: + - dialect: ANSI_SQL + expression: R_NAME + - name: R_COMMENT + expression: + dialects: + - dialect: ANSI_SQL + expression: R_COMMENT + concept_mappings: + - concept: Region + object_mappings: + - referent_mappings: + - relationship: region_key + expression: KSE_TPCH_SF1_REGION.R_REGIONKEY + link_mappings: + - object_mapping: + referent_mappings: + - relationship: region_key + expression: KSE_TPCH_SF1_REGION.R_REGIONKEY + children: + - object_mapping: + concept: String + expression: KSE_TPCH_SF1_REGION.R_NAME + relationship: region_name + - object_mapping: + concept: String + expression: KSE_TPCH_SF1_REGION.R_COMMENT + relationship: region_comment + - concept: Nation + object_mappings: + - referent_mappings: + - relationship: nation_key + expression: KSE_TPCH_SF1_NATION.N_NATIONKEY + link_mappings: + - object_mapping: + referent_mappings: + - relationship: nation_key + expression: KSE_TPCH_SF1_NATION.N_NATIONKEY + children: + - object_mapping: + concept: String + expression: KSE_TPCH_SF1_NATION.N_NAME + relationship: nation_name + - object_mapping: + concept: String + expression: KSE_TPCH_SF1_NATION.N_COMMENT + relationship: nation_comment + - object_mapping: + concept: Integer + expression: KSE_TPCH_SF1_NATION.N_REGIONKEY + relationship: region_key + - concept: Supplier + object_mappings: + - referent_mappings: + - relationship: supplier_key + expression: KSE_TPCH_SF1_SUPPLIER.S_SUPPKEY + link_mappings: + - object_mapping: + referent_mappings: + - relationship: supplier_key + expression: KSE_TPCH_SF1_SUPPLIER.S_SUPPKEY + children: + - object_mapping: + concept: String + expression: KSE_TPCH_SF1_SUPPLIER.S_NAME + relationship: supplier_name + - object_mapping: + concept: String + expression: KSE_TPCH_SF1_SUPPLIER.S_ADDRESS + relationship: supplier_address + - object_mapping: + concept: String + expression: KSE_TPCH_SF1_SUPPLIER.S_PHONE + relationship: supplier_phone + - object_mapping: + concept: Decimal + expression: KSE_TPCH_SF1_SUPPLIER.S_ACCTBAL + relationship: supplier_account_balance + - object_mapping: + concept: String + expression: KSE_TPCH_SF1_SUPPLIER.S_COMMENT + relationship: supplier_comment + - object_mapping: + concept: Integer + expression: KSE_TPCH_SF1_SUPPLIER.S_NATIONKEY + relationship: nation_key + - concept: Part + object_mappings: + - referent_mappings: + - relationship: part_key + expression: KSE_TPCH_SF1_PART.P_PARTKEY + link_mappings: + - object_mapping: + referent_mappings: + - relationship: part_key + expression: KSE_TPCH_SF1_PART.P_PARTKEY + children: + - object_mapping: + concept: String + expression: KSE_TPCH_SF1_PART.P_NAME + relationship: part_name + - object_mapping: + concept: String + expression: KSE_TPCH_SF1_PART.P_MFGR + relationship: part_manufacturer_name + - object_mapping: + concept: String + expression: KSE_TPCH_SF1_PART.P_BRAND + relationship: part_brand + - object_mapping: + concept: String + expression: KSE_TPCH_SF1_PART.P_TYPE + relationship: part_type + - object_mapping: + concept: Integer + expression: KSE_TPCH_SF1_PART.P_SIZE + relationship: part_size + - object_mapping: + concept: String + expression: KSE_TPCH_SF1_PART.P_CONTAINER + relationship: part_container + - object_mapping: + concept: Decimal + expression: KSE_TPCH_SF1_PART.P_RETAILPRICE + relationship: part_retail_price + - object_mapping: + concept: String + expression: KSE_TPCH_SF1_PART.P_COMMENT + relationship: part_comment + - concept: PartSupplier + object_mappings: + - referent_mappings: + - relationship: part_key + expression: KSE_TPCH_SF1_PARTSUPP.PS_PARTKEY + - relationship: supplier_key + expression: KSE_TPCH_SF1_PARTSUPP.PS_SUPPKEY + link_mappings: + - object_mapping: + referent_mappings: + - relationship: part_key + expression: KSE_TPCH_SF1_PARTSUPP.PS_PARTKEY + - relationship: supplier_key + expression: KSE_TPCH_SF1_PARTSUPP.PS_SUPPKEY + children: + - object_mapping: + concept: Decimal + expression: KSE_TPCH_SF1_PARTSUPP.PS_AVAILQTY + relationship: available_quantity + - object_mapping: + concept: Decimal + expression: KSE_TPCH_SF1_PARTSUPP.PS_SUPPLYCOST + relationship: part_supplier_supply_cost + - object_mapping: + concept: String + expression: KSE_TPCH_SF1_PARTSUPP.PS_COMMENT + relationship: part_supplier_comment + - concept: CustOrder + object_mappings: + - referent_mappings: + - relationship: order_key + expression: KSE_TPCH_SF1_ORDERS.O_ORDERKEY + link_mappings: + - object_mapping: + referent_mappings: + - relationship: order_key + expression: KSE_TPCH_SF1_ORDERS.O_ORDERKEY + children: + - object_mapping: + concept: Decimal + expression: KSE_TPCH_SF1_ORDERS.O_TOTALPRICE + relationship: order_gross_total + - object_mapping: + concept: String + expression: KSE_TPCH_SF1_ORDERS.O_CLERK + relationship: order_clerk + - object_mapping: + concept: Decimal + expression: KSE_TPCH_SF1_ORDERS.O_SHIPPRIORITY + relationship: order_ship_priority + - object_mapping: + concept: String + expression: KSE_TPCH_SF1_ORDERS.O_COMMENT + relationship: order_comment + - object_mapping: + concept: Integer + expression: KSE_TPCH_SF1_ORDERS.O_CUSTKEY + relationship: customer_key + - object_mapping: + concept: String + expression: KSE_TPCH_SF1_ORDERS.O_ORDERSTATUS + relationship: order_status + - object_mapping: + concept: String + expression: KSE_TPCH_SF1_ORDERS.O_ORDERPRIORITY + relationship: order_priority + - object_mapping: + concept: Date + expression: KSE_TPCH_SF1_ORDERS.O_ORDERDATE + relationship: order_date + - concept: OrderLineitem + object_mappings: + - referent_mappings: + - relationship: order_key + expression: KSE_TPCH_SF1_LINEITEM.L_ORDERKEY + - relationship: order_lineitem_number + expression: KSE_TPCH_SF1_LINEITEM.L_LINENUMBER + link_mappings: + - object_mapping: + referent_mappings: + - relationship: order_key + expression: KSE_TPCH_SF1_LINEITEM.L_ORDERKEY + - relationship: order_lineitem_number + expression: KSE_TPCH_SF1_LINEITEM.L_LINENUMBER + children: + - object_mapping: + concept: Decimal + expression: KSE_TPCH_SF1_LINEITEM.L_QUANTITY + relationship: lineitem_quantity + - object_mapping: + concept: Decimal + expression: KSE_TPCH_SF1_LINEITEM.L_EXTENDEDPRICE + relationship: lineitem_extended_price + - object_mapping: + concept: Decimal + expression: KSE_TPCH_SF1_LINEITEM.L_DISCOUNT + relationship: lineitem_discount + - object_mapping: + concept: Decimal + expression: KSE_TPCH_SF1_LINEITEM.L_TAX + relationship: lineitem_tax + - object_mapping: + concept: String + expression: KSE_TPCH_SF1_LINEITEM.L_COMMENT + relationship: lineitem_comment + - object_mapping: + concept: Integer + expression: KSE_TPCH_SF1_LINEITEM.L_PARTKEY + relationship: part_key + - object_mapping: + concept: Integer + expression: KSE_TPCH_SF1_LINEITEM.L_SUPPKEY + relationship: supplier_key + - object_mapping: + concept: String + expression: KSE_TPCH_SF1_LINEITEM.L_RETURNFLAG + relationship: lineitem_return_flag + - object_mapping: + concept: String + expression: KSE_TPCH_SF1_LINEITEM.L_LINESTATUS + relationship: lineitem_status + - object_mapping: + concept: String + expression: KSE_TPCH_SF1_LINEITEM.L_SHIPINSTRUCT + relationship: lineitem_shipping_instructions + - object_mapping: + concept: String + expression: KSE_TPCH_SF1_LINEITEM.L_SHIPMODE + relationship: lineitem_shipping_mode + - object_mapping: + concept: Date + expression: KSE_TPCH_SF1_LINEITEM.L_SHIPDATE + relationship: lineitem_ship_date + - object_mapping: + concept: Date + expression: KSE_TPCH_SF1_LINEITEM.L_COMMITDATE + relationship: lineitem_commit_date + - object_mapping: + concept: Date + expression: KSE_TPCH_SF1_LINEITEM.L_RECEIPTDATE + relationship: lineitem_receipt_date + - concept: Customer + object_mappings: + - referent_mappings: + - relationship: customer_key + expression: KSE_TPCH_SF1_CUSTOMER.C_CUSTKEY + link_mappings: + - object_mapping: + referent_mappings: + - relationship: customer_key + expression: KSE_TPCH_SF1_CUSTOMER.C_CUSTKEY + children: + - object_mapping: + concept: String + expression: KSE_TPCH_SF1_CUSTOMER.C_NAME + relationship: customer_name + - object_mapping: + concept: String + expression: KSE_TPCH_SF1_CUSTOMER.C_ADDRESS + relationship: customer_address + - object_mapping: + concept: String + expression: KSE_TPCH_SF1_CUSTOMER.C_PHONE + relationship: customer_phone + - object_mapping: + concept: Decimal + expression: KSE_TPCH_SF1_CUSTOMER.C_ACCTBAL + relationship: customer_account_balance + - object_mapping: + concept: String + expression: KSE_TPCH_SF1_CUSTOMER.C_MKTSEGMENT + relationship: customer_market_segment + - object_mapping: + concept: String + expression: KSE_TPCH_SF1_CUSTOMER.C_COMMENT + relationship: customer_comment + - object_mapping: + concept: Integer + expression: KSE_TPCH_SF1_CUSTOMER.C_NATIONKEY + relationship: nation_key \ No newline at end of file diff --git a/converters/ontology/tests/fixtures/validation/ambiguous_type_vars.yaml b/converters/ontology/tests/fixtures/validation/ambiguous_type_vars.yaml new file mode 100644 index 00000000..d1ced406 --- /dev/null +++ b/converters/ontology/tests/fixtures/validation/ambiguous_type_vars.yaml @@ -0,0 +1,41 @@ +# 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. + +version: 0.2.0.dev0 +name: Sample model +ontology: + - concept: Person + type: EntityType + identify_by: [ssn] + relationships: + - name: ssn + roles: + - concept: SocialSecurityNr + multiplicity: OneToOne + derived_by: ['Person.earns(var_name) AND Person.ssn(var_name)'] + - name: earns + roles: + - concept: Salary + multiplicity: ManyToOne + - concept: SocialSecurityNr + type: ValueType + extends: [Integer] + requires: ['0 < SocialSecurityNr AND SocialSecurityNr <= 999999999'] + - concept: Salary + type: ValueType + extends: [Decimal] + requires: ['Salary > 0'] \ No newline at end of file diff --git a/converters/ontology/tests/fixtures/validation/bare_field_in_formula.yaml b/converters/ontology/tests/fixtures/validation/bare_field_in_formula.yaml new file mode 100644 index 00000000..36783552 --- /dev/null +++ b/converters/ontology/tests/fixtures/validation/bare_field_in_formula.yaml @@ -0,0 +1,39 @@ +# 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. + +version: 0.2.0.dev0 +name: Sample model +ontology: + - concept: Person + type: EntityType + identify_by: [ssn] + relationships: + - name: ssn + roles: + - concept: SocialSecurityNr + multiplicity: OneToOne + - name: earns + roles: + - concept: Salary + multiplicity: ManyToOne + derived_by: ['Person.earns(Salary(sal))'] + - concept: SocialSecurityNr + type: ValueType + extends: [Integer] + - concept: Salary + type: ValueType + extends: [Decimal] \ No newline at end of file diff --git a/converters/ontology/tests/fixtures/validation/mapping_formula_agg_with_rel.yaml b/converters/ontology/tests/fixtures/validation/mapping_formula_agg_with_rel.yaml new file mode 100644 index 00000000..65081d05 --- /dev/null +++ b/converters/ontology/tests/fixtures/validation/mapping_formula_agg_with_rel.yaml @@ -0,0 +1,80 @@ +# 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. + +version: 0.2.0.dev0 +name: Sample model with agg formula referencing a relationship +ontology: + - concept: TotalSales + type: ValueType + extends: [Decimal] + - concept: Item + type: EntityType + identify_by: [itemId] + relationships: + - name: itemId + roles: + - concept: Integer + multiplicity: OneToOne + - name: grossReturns + roles: + - concept: Store + - concept: Decimal + multiplicity: ManyToOne + - name: weekSales + roles: + - concept: TotalSales + multiplicity: ManyToOne + - concept: Store + type: EntityType + identify_by: [storeId] + relationships: + - name: storeId + roles: + - concept: Integer + multiplicity: OneToOne +ontology_mappings: + - name: mapping + semantic_model: + name: sales_model + datasets: + - name: SALES + source: SALES + fields: + - name: item_id + expression: {} + - name: amount + expression: {} + concept_mappings: + - concept: Item + object_mappings: + - referent_mappings: + - relationship: itemId + expression: SALES.item_id + link_mappings: + - object_mapping: + referent_mappings: + - relationship: itemId + expression: SALES.item_id + children: + - relationship: weekSales + object_mapping: + expression: 'SUM[SALES.amount WHERE Item.grossReturns(Store) > 0 GROUP BY SALES.item_id]' + - concept: Store + object_mappings: + - referent_mappings: + - relationship: storeId + expression: SALES.item_id \ No newline at end of file diff --git a/converters/ontology/tests/fixtures/validation/mapping_formula_concept_ref.yaml b/converters/ontology/tests/fixtures/validation/mapping_formula_concept_ref.yaml new file mode 100644 index 00000000..499c61b9 --- /dev/null +++ b/converters/ontology/tests/fixtures/validation/mapping_formula_concept_ref.yaml @@ -0,0 +1,65 @@ +# 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. + +version: 0.2.0.dev0 +name: Sample model with concept ref in mapping formula +ontology: + - concept: PaymentId + type: ValueType + extends: [Integer] + - concept: Amount + type: ValueType + extends: [Decimal] + - concept: CashPayment + type: EntityType + identify_by: [paymentId] + relationships: + - name: paymentId + roles: + - concept: PaymentId + multiplicity: OneToOne + - name: netAmount + roles: + - concept: Amount + multiplicity: ManyToOne +ontology_mappings: + - name: mapping + semantic_model: + name: payments_model + datasets: + - name: PAYMENTS + source: PAYMENTS + fields: + - name: id + expression: {} + - name: cashReceived + expression: {} + concept_mappings: + - concept: CashPayment + object_mappings: + - referent_mappings: + - relationship: paymentId + expression: PAYMENTS.id + link_mappings: + - object_mapping: + referent_mappings: + - relationship: paymentId + expression: PAYMENTS.id + children: + - relationship: netAmount + object_mapping: + expression: PAYMENTS.cashReceived + Amount \ No newline at end of file diff --git a/converters/ontology/tests/fixtures/validation/mapping_formula_unknown_field.yaml b/converters/ontology/tests/fixtures/validation/mapping_formula_unknown_field.yaml new file mode 100644 index 00000000..bd45a4cd --- /dev/null +++ b/converters/ontology/tests/fixtures/validation/mapping_formula_unknown_field.yaml @@ -0,0 +1,65 @@ +# 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. + +version: 0.2.0.dev0 +name: Sample model +ontology: + - concept: PaymentId + type: ValueType + extends: [Integer] + - concept: Amount + type: ValueType + extends: [Decimal] + - concept: CashPayment + type: EntityType + identify_by: [paymentId] + relationships: + - name: paymentId + roles: + - concept: PaymentId + multiplicity: OneToOne + - name: netAmount + roles: + - concept: Amount + multiplicity: ManyToOne +ontology_mappings: + - name: mapping + semantic_model: + name: payments_model + datasets: + - name: PAYMENTS + source: PAYMENTS + fields: + - name: id + expression: {} + - name: cashReceived + expression: {} + concept_mappings: + - concept: CashPayment + object_mappings: + - referent_mappings: + - relationship: paymentId + expression: PAYMENTS.id + link_mappings: + - object_mapping: + referent_mappings: + - relationship: paymentId + expression: PAYMENTS.id + children: + - relationship: netAmount + object_mapping: + expression: PAYMENTS.cashReceived - PAYMENTS.unknownField \ No newline at end of file diff --git a/converters/ontology/tests/fixtures/validation/mapping_formula_valid.yaml b/converters/ontology/tests/fixtures/validation/mapping_formula_valid.yaml new file mode 100644 index 00000000..1a755e7a --- /dev/null +++ b/converters/ontology/tests/fixtures/validation/mapping_formula_valid.yaml @@ -0,0 +1,67 @@ +# 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. + +version: 0.2.0.dev0 +name: Sample model +ontology: + - concept: PaymentId + type: ValueType + extends: [Integer] + - concept: Amount + type: ValueType + extends: [Decimal] + - concept: CashPayment + type: EntityType + identify_by: [paymentId] + relationships: + - name: paymentId + roles: + - concept: PaymentId + multiplicity: OneToOne + - name: netAmount + roles: + - concept: Amount + multiplicity: ManyToOne +ontology_mappings: + - name: mapping + semantic_model: + name: payments_model + datasets: + - name: PAYMENTS + source: PAYMENTS + fields: + - name: id + expression: {} + - name: cashReceived + expression: {} + - name: change + expression: {} + concept_mappings: + - concept: CashPayment + object_mappings: + - referent_mappings: + - relationship: paymentId + expression: PAYMENTS.id + link_mappings: + - object_mapping: + referent_mappings: + - relationship: paymentId + expression: PAYMENTS.id + children: + - relationship: netAmount + object_mapping: + expression: PAYMENTS.cashReceived - PAYMENTS.change \ No newline at end of file diff --git a/converters/ontology/tests/fixtures/validation/not_declared_concept_in_identifier.yaml b/converters/ontology/tests/fixtures/validation/not_declared_concept_in_identifier.yaml new file mode 100644 index 00000000..15c65111 --- /dev/null +++ b/converters/ontology/tests/fixtures/validation/not_declared_concept_in_identifier.yaml @@ -0,0 +1,23 @@ +# 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. + +version: 0.2.0.dev0 +name: Sample model +ontology: + - concept: Person + type: EntityType + identify_by: [ssn] \ No newline at end of file diff --git a/converters/ontology/tests/fixtures/validation/not_declared_concept_in_relationship.yaml b/converters/ontology/tests/fixtures/validation/not_declared_concept_in_relationship.yaml new file mode 100644 index 00000000..4416818d --- /dev/null +++ b/converters/ontology/tests/fixtures/validation/not_declared_concept_in_relationship.yaml @@ -0,0 +1,42 @@ +# 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. + +version: 0.2.0.dev0 +name: Sample model +ontology: + - concept: Person + type: EntityType + identify_by: [ssn] + relationships: + - name: ssn + roles: + - concept: SocialSecurityNr + multiplicity: OneToOne + - name: earns + roles: + - concept: Salary + multiplicity: ManyToOne + - name: live_in + roles: + - concept: City + multiplicity: ManyToOne + - concept: SocialSecurityNr + type: ValueType + extends: [Integer] + - concept: Salary + type: ValueType + extends: [Decimal] \ No newline at end of file diff --git a/converters/ontology/tests/fixtures/validation/require_sub_exists_unknown_rel.yaml b/converters/ontology/tests/fixtures/validation/require_sub_exists_unknown_rel.yaml new file mode 100644 index 00000000..9bcd5b5e --- /dev/null +++ b/converters/ontology/tests/fixtures/validation/require_sub_exists_unknown_rel.yaml @@ -0,0 +1,54 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +version: 0.2.0.dev0 +name: Sample model +ontology: + - concept: Person + type: EntityType + identify_by: [ssn] + requires: + - 'Person.name AND EXISTS(Company.turnover > 0)' + relationships: + - name: ssn + roles: [{concept: SocialSecurityNr}] + multiplicity: OneToOne + - name: name + roles: [{concept: PersonName}] + multiplicity: ManyToOne + - concept: Company + type: EntityType + identify_by: [taxId] + relationships: + - name: taxId + roles: [{concept: TaxId}] + multiplicity: OneToOne + - name: revenue + roles: [{concept: Revenue}] + multiplicity: ManyToOne + - concept: SocialSecurityNr + type: ValueType + extends: [Integer] + - concept: PersonName + type: ValueType + extends: [String] + - concept: TaxId + type: ValueType + extends: [Integer] + - concept: Revenue + type: ValueType + extends: [Decimal] diff --git a/converters/ontology/tests/fixtures/validation/require_sub_exists_valid.yaml b/converters/ontology/tests/fixtures/validation/require_sub_exists_valid.yaml new file mode 100644 index 00000000..1636d624 --- /dev/null +++ b/converters/ontology/tests/fixtures/validation/require_sub_exists_valid.yaml @@ -0,0 +1,56 @@ +# 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. + +version: 0.2.0.dev0 +name: Sample model +ontology: + - concept: Person + type: EntityType + identify_by: [ssn] + requires: + - 'Person.name AND EXISTS(Company.revenue > 0)' + relationships: + - name: ssn + roles: [{concept: SocialSecurityNr}] + multiplicity: OneToOne + - name: name + roles: [{concept: PersonName}] + multiplicity: ManyToOne + - concept: Company + type: EntityType + identify_by: [taxId] + relationships: + - name: taxId + roles: [{concept: TaxId}] + multiplicity: OneToOne + - name: revenue + roles: [{concept: Revenue}] + multiplicity: ManyToOne + requires: + - 'EXISTS(Person.name)' + - concept: SocialSecurityNr + type: ValueType + extends: [Integer] + - concept: PersonName + type: ValueType + extends: [String] + - concept: TaxId + type: ValueType + extends: [Integer] + - concept: Revenue + type: ValueType + extends: [Decimal] diff --git a/converters/ontology/tests/fixtures/validation/scoped_require_scope_valid.yaml b/converters/ontology/tests/fixtures/validation/scoped_require_scope_valid.yaml new file mode 100644 index 00000000..0ffb7850 --- /dev/null +++ b/converters/ontology/tests/fixtures/validation/scoped_require_scope_valid.yaml @@ -0,0 +1,158 @@ +# 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. + +version: 0.2.0.dev0 +name: Sample model +ontology: + - concept: Person + type: EntityType + identify_by: [ssn] + requires: + # `Company` is a second concept, but it is connected to the Person scope + # through the `employer` fact, so it is bound (no EXISTS needed). + - 'Person.employer(Company) AND Company.revenue > 0' + # The same join written the other way around: the fact reaches the scope + # even though it is not the first conjunct that mentions it. + - 'Company.revenue > 0 AND Person.employer(Company)' + # A binder nested in another binder still binds what it encloses. + - 'Person.ssn > 0 AND EXISTS(Company.revenue > 0 AND EXISTS(Person.employer(Company)))' + # Scope travels down a chain of facts, in either direction. `sub` is a + # var, so 'sub.staff' only resolves once inference types it from the fact + # — the step after the dot names a relationship, never a free variable. + - 'Person.employer(Company) AND Company.subsidiary(sub) AND sub.staff > 0' + - 'Company.subsidiary(sub) AND sub.staff > 0 AND Person.employer(Company)' + # A ternary binds every role it names. + - 'Person.employment(Company, Position) AND Position.level > 3' + # The same ternary applied to all but its last role, continuing the + # traversal from the Position that plays it. + - 'Person.employment(Company).level > 3' + # An aggregate is a binder, so what it encloses needs no join to the scope. + - 'Person.name AND COUNT[Product] > 0' + relationships: + - name: ssn + roles: [{concept: SocialSecurityNr}] + multiplicity: OneToOne + - name: monthly_income + roles: [{concept: Money}] + multiplicity: ManyToOne + - name: annual_income + roles: [{concept: Money, name: annual_income}] + multiplicity: ManyToOne + # `monthly` is a local variable: the equality defines it from the + # in-scope `Person`, which puts it in scope for the second conjunct. + derived_by: + - 'monthly == Person.monthly_income AND annual_income == monthly * 12' + - name: name + roles: [{concept: PersonName}] + multiplicity: ManyToOne + - name: employer + roles: [{concept: Company}] + multiplicity: ManyToOne + # A relationship-scoped formula may reference every role player of its + # parent relationship — here both `Person` and `Company`. + requires: + - 'Person.ssn > 0 AND Company.revenue > 0' + - name: employment + roles: [{concept: Company}, {concept: Position}] + multiplicity: ManyToOne + - concept: Company + type: EntityType + identify_by: [taxId] + relationships: + - name: taxId + roles: [{concept: TaxId}] + multiplicity: OneToOne + - name: revenue + roles: [{concept: Revenue}] + multiplicity: ManyToOne + - name: subsidiary + roles: [{concept: Subsidiary}] + multiplicity: ManyToOne + - concept: Subsidiary + type: EntityType + identify_by: [subId] + relationships: + - name: subId + roles: [{concept: TaxId}] + multiplicity: OneToOne + - name: staff + roles: [{concept: Headcount}] + multiplicity: ManyToOne + - concept: Position + type: EntityType + identify_by: [title] + relationships: + - name: title + roles: [{concept: PositionTitle}] + multiplicity: OneToOne + - name: level + roles: [{concept: Headcount}] + multiplicity: ManyToOne + - concept: Product + type: EntityType + identify_by: [sku] + relationships: + - name: sku + roles: [{concept: Sku}] + multiplicity: OneToOne + - concept: Payment + type: EntityType + identify_by: [paymentId] + relationships: + - name: paymentId + roles: [{concept: PaymentId}] + multiplicity: OneToOne + - name: type + roles: [{concept: PaymentType}] + multiplicity: ManyToOne + - concept: Card + type: EntityType + extends: [Payment] + # `Card` is defined by carving from its supertype `Payment`; `Payment` is + # in scope even though it is not the parent concept. + derived_by: + - "Payment.type == 'card'" + - concept: Money + type: ValueType + extends: [Decimal] + - concept: PersonName + type: ValueType + extends: [String] + - concept: PositionTitle + type: ValueType + extends: [String] + - concept: Headcount + type: ValueType + extends: [Integer] + - concept: Sku + type: ValueType + extends: [Integer] + - concept: SocialSecurityNr + type: ValueType + extends: [Integer] + - concept: TaxId + type: ValueType + extends: [Integer] + - concept: Revenue + type: ValueType + extends: [Decimal] + - concept: PaymentId + type: ValueType + extends: [Integer] + - concept: PaymentType + type: ValueType + extends: [String] \ No newline at end of file diff --git a/converters/ontology/tests/fixtures/validation/signature_mismatch.yaml b/converters/ontology/tests/fixtures/validation/signature_mismatch.yaml new file mode 100644 index 00000000..a0183d94 --- /dev/null +++ b/converters/ontology/tests/fixtures/validation/signature_mismatch.yaml @@ -0,0 +1,41 @@ +# 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. + +version: 0.2.0.dev0 +name: Sample model +ontology: + - concept: Person + type: EntityType + identify_by: [ssn] + relationships: + - name: ssn + roles: + - concept: SocialSecurityNr + multiplicity: OneToOne + - name: earns + roles: + - concept: Salary + multiplicity: ManyToOne + derived_by: ['Person.earns(Salary) AND Person.ssn(Salary)'] + - concept: SocialSecurityNr + type: ValueType + extends: [Integer] + requires: ['0 < SocialSecurityNr AND SocialSecurityNr <= 999999999'] + - concept: Salary + type: ValueType + extends: [Decimal] + requires: ['Salary > 0'] \ No newline at end of file diff --git a/converters/ontology/tests/fixtures/validation/top_level_require_exists_valid.yaml b/converters/ontology/tests/fixtures/validation/top_level_require_exists_valid.yaml new file mode 100644 index 00000000..ff76123a --- /dev/null +++ b/converters/ontology/tests/fixtures/validation/top_level_require_exists_valid.yaml @@ -0,0 +1,33 @@ +# 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. + +version: 0.2.0.dev0 +name: Sample model +requires: + - 'EXISTS(Person.ssn(s) AND s > 0)' +ontology: + - concept: Person + type: EntityType + identify_by: [ssn] + relationships: + - name: ssn + roles: + - concept: SocialSecurityNr + multiplicity: OneToOne + - concept: SocialSecurityNr + type: ValueType + extends: [Integer] \ No newline at end of file diff --git a/converters/ontology/tests/fixtures/validation/top_level_require_free_var.yaml b/converters/ontology/tests/fixtures/validation/top_level_require_free_var.yaml new file mode 100644 index 00000000..70f3779b --- /dev/null +++ b/converters/ontology/tests/fixtures/validation/top_level_require_free_var.yaml @@ -0,0 +1,33 @@ +# 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. + +version: 0.2.0.dev0 +name: Sample model +requires: + - 'Person.ssn > 0' +ontology: + - concept: Person + type: EntityType + identify_by: [ssn] + relationships: + - name: ssn + roles: + - concept: SocialSecurityNr + multiplicity: OneToOne + - concept: SocialSecurityNr + type: ValueType + extends: [Integer] \ No newline at end of file diff --git a/converters/ontology/tests/fixtures/validation/top_level_require_unknown_concept.yaml b/converters/ontology/tests/fixtures/validation/top_level_require_unknown_concept.yaml new file mode 100644 index 00000000..8e831a40 --- /dev/null +++ b/converters/ontology/tests/fixtures/validation/top_level_require_unknown_concept.yaml @@ -0,0 +1,33 @@ +# 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. + +version: 0.2.0.dev0 +name: Sample model +requires: + - 'COUNT[Nonexistent] > 0' +ontology: + - concept: Person + type: EntityType + identify_by: [ssn] + relationships: + - name: ssn + roles: + - concept: SocialSecurityNr + multiplicity: OneToOne + - concept: SocialSecurityNr + type: ValueType + extends: [Integer] \ No newline at end of file diff --git a/converters/ontology/tests/fixtures/validation/top_level_require_valid.yaml b/converters/ontology/tests/fixtures/validation/top_level_require_valid.yaml new file mode 100644 index 00000000..b7f383ae --- /dev/null +++ b/converters/ontology/tests/fixtures/validation/top_level_require_valid.yaml @@ -0,0 +1,33 @@ +# 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. + +version: 0.2.0.dev0 +name: Sample model +requires: + - 'COUNT[Person] > 0' +ontology: + - concept: Person + type: EntityType + identify_by: [ssn] + relationships: + - name: ssn + roles: + - concept: SocialSecurityNr + multiplicity: OneToOne + - concept: SocialSecurityNr + type: ValueType + extends: [Integer] \ No newline at end of file diff --git a/converters/ontology/tests/fixtures/validation/undefined_relationship.yaml b/converters/ontology/tests/fixtures/validation/undefined_relationship.yaml new file mode 100644 index 00000000..30bf340b --- /dev/null +++ b/converters/ontology/tests/fixtures/validation/undefined_relationship.yaml @@ -0,0 +1,39 @@ +# 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. + +version: 0.2.0.dev0 +name: Sample model +ontology: + - concept: Person + type: EntityType + identify_by: [ssn] + relationships: + - name: ssn + roles: + - concept: SocialSecurityNr + multiplicity: OneToOne + derived_by: ['Person.gain(Salary)'] + - name: earns + roles: + - concept: Salary + multiplicity: ManyToOne + - concept: SocialSecurityNr + type: ValueType + extends: [Integer] + - concept: Salary + type: ValueType + extends: [Decimal] \ No newline at end of file diff --git a/converters/ontology/tests/fixtures/validation/unresolved_var_type.yaml b/converters/ontology/tests/fixtures/validation/unresolved_var_type.yaml new file mode 100644 index 00000000..5511aaf5 --- /dev/null +++ b/converters/ontology/tests/fixtures/validation/unresolved_var_type.yaml @@ -0,0 +1,39 @@ +# 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. + +version: 0.2.0.dev0 +name: Sample model +ontology: + - concept: Person + type: EntityType + identify_by: [ssn] + relationships: + - name: ssn + roles: + - concept: SocialSecurityNr + multiplicity: OneToOne + - name: earns + roles: + - concept: Salary + multiplicity: ManyToOne + derived_by: ['s > 0 AND Person.earns(Salary)'] + - concept: SocialSecurityNr + type: ValueType + extends: [Integer] + - concept: Salary + type: ValueType + extends: [Decimal] \ No newline at end of file diff --git a/converters/ontology/tests/model/__init__.py b/converters/ontology/tests/model/__init__.py new file mode 100644 index 00000000..15cdc54d --- /dev/null +++ b/converters/ontology/tests/model/__init__.py @@ -0,0 +1,18 @@ +# 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. + +"""The ontology model itself: concepts and their lookup, roles, verbalizations.""" diff --git a/converters/ontology/tests/test_concept_lookup.py b/converters/ontology/tests/model/test_concept_lookup.py similarity index 100% rename from converters/ontology/tests/test_concept_lookup.py rename to converters/ontology/tests/model/test_concept_lookup.py diff --git a/converters/ontology/tests/test_roles.py b/converters/ontology/tests/model/test_roles.py similarity index 100% rename from converters/ontology/tests/test_roles.py rename to converters/ontology/tests/model/test_roles.py diff --git a/converters/ontology/tests/test_verbalization_parsing.py b/converters/ontology/tests/model/test_verbalization.py similarity index 100% rename from converters/ontology/tests/test_verbalization_parsing.py rename to converters/ontology/tests/model/test_verbalization.py diff --git a/converters/ontology/tests/palantir/__init__.py b/converters/ontology/tests/palantir/__init__.py new file mode 100644 index 00000000..33f5dcda --- /dev/null +++ b/converters/ontology/tests/palantir/__init__.py @@ -0,0 +1,24 @@ +# 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. + +"""The Palantir spoke: reading an export, and converting what it holds. + +`test_parser` covers reading — an export arrives as a folder or a zip, in +layouts that vary, and the parser has to find its way around one. `converter` +covers what happens next: which of an export's object types, properties and +relations belong in an ontology at all. +""" diff --git a/converters/ontology/tests/palantir/converter/__init__.py b/converters/ontology/tests/palantir/converter/__init__.py new file mode 100644 index 00000000..3c618708 --- /dev/null +++ b/converters/ontology/tests/palantir/converter/__init__.py @@ -0,0 +1,38 @@ +# 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. + +"""What the Palantir converter admits into the model, and why. + +Two things decide it. The first is *status*: an export marks each object type, +property and relation as active, experimental, deprecated and so on, and only +some of those belong in a converted model. The defaults suit an ontology in +production use -- active work, plus the endorsed and intermediary entities it +leans on. They are class attributes read through ``allowed_*_statuses()`` +methods rather than inline checks, so a caller with a different definition of +"finished" (a draft ontology, say, where nearly everything is still +experimental) subclasses and widens them. These suites pin both the defaults and +the overriding. + +The second is *column matching*: an export's two halves routinely disagree on +the case of a physical column name, so a primary key naming ``LOCNO`` has to +find a dataset field called ``locno``. Exact matches still win. + +Each test builds the smallest export that shows the behaviour and runs it +through the real parser, so the fixtures stay in the JSON shapes an export +actually uses. `builders` describes those shapes; `helpers` converts one and +reads the result back. +""" diff --git a/converters/ontology/tests/palantir/converter/builders.py b/converters/ontology/tests/palantir/converter/builders.py new file mode 100644 index 00000000..39380748 --- /dev/null +++ b/converters/ontology/tests/palantir/converter/builders.py @@ -0,0 +1,183 @@ +# 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. + +"""Builders for Palantir export JSON. + +The export format is described in one place here rather than in each suite that +needs it: the parser suite next door reads exports off disk and the suites in +this package convert them, and both otherwise know the same nesting of rids, +`baseType` wrappers and `primaryKeyMapping` spellings. A change to the format is +then a change to this file. +""" + +from __future__ import annotations + +from typing import Any, Iterable + +def prop( + rid: str, + id_: str, + *, + status: str | None = None, + column: str | None = None, + pk_columns: dict[str, str] | None = None, + type_name: str = "STRING", +) -> dict[str, Any]: + prop: dict[str, Any] = {"rid": rid, "id": id_, "baseType": {"type": type_name}} + if status: + prop["status"] = {"type": status} + if column: + prop["column"] = column + if pk_columns: + # Keyed by the backing dataset's rid: a primary key can sit in a + # differently named column in each dataset that feeds the object type. + prop["primaryKeyMapping"] = { + ds_rid: {"columnName": name} for ds_rid, name in pk_columns.items() + } + return prop + +def object_type( + key: str, + name: str, + *, + status: str | None = None, + properties: Iterable[dict[str, Any]] = (), + primary_keys: Iterable[str] | None = None, + datasources: Iterable[str] | None = None, +) -> dict[str, Any]: + """An object type identified by ``_id``, plus any *properties* given. + + With no *datasources*, carries no ``datasources`` entry, so the parser falls + back to matching a dataset whose ``mainDatasetId`` is the object type's own + rid — which is what :func:`_dataset` builds. Pass dataset rids to back the + object type with several datasets instead. + """ + props = [prop(f"ri.p.{key}_id", f"{key}_id"), *properties] + ot: dict[str, Any] = { + "rid": ot_rid(key), + "id": key, + "displayName": name, + "properties": props, + "primaryKeys": list(primary_keys) if primary_keys is not None else [f"{key}_id"], + } + if status: + ot["status"] = {"type": status} + if datasources is not None: + ot["datasources"] = [ + {"datasourceRid": rid, "backingResourceRid": rid} for rid in datasources + ] + return ot + +def many_to_one( + key: str, + *, + one: str, + many: str, + key_map: dict[str, str], + status: str | None = None, +) -> dict[str, Any]: + """A M:1 relation from the *many* object type to the *one* object type. + + ``key_map`` maps a primary-key property rid on the one side to the property + rid holding it on the many side. + """ + relation: dict[str, Any] = { + "rid": f"ri.rel.{key}", + "id": key, + "definition": { + "type": "oneToMany", + "oneToMany": { + "objectTypeRidOneSide": ot_rid(one), + "objectTypeRidManySide": ot_rid(many), + "oneSidePrimaryKeyToManySidePropertyMapping": key_map, + }, + }, + } + if status: + relation["status"] = {"type": status} + return relation + +def many_to_many(key: str, *, a: str, b: str, status: str | None = None) -> dict[str, Any]: + """A M:M relation between object types *a* and *b*, keyed on their own ids. + + Each side's primary key maps to the like-named column of the join table, + which the relation carries as its backing datasource. + """ + relation: dict[str, Any] = { + "rid": f"ri.rel.{key}", + "id": key, + "definition": { + "type": "manyToMany", + "manyToMany": { + "objectTypeRidA": ot_rid(a), + "objectTypeRidB": ot_rid(b), + "objectTypeAPrimaryKeyPropertyMapping": {f"ri.p.{a}_id": f"{a}_id"}, + "objectTypeBPrimaryKeyPropertyMapping": {f"ri.p.{b}_id": f"{b}_id"}, + "joinTableDatasource": [ + {"datasourceRid": f"ri.ds.{key}", "backingResourceRid": f"ri.res.{key}"} + ], + }, + }, + } + if status: + relation["status"] = {"type": status} + return relation + +def intermediary( + key: str, + *, + a: str, + b: str, + via: str, + link_a: str, + link_b: str, + status: str | None = None, +) -> dict[str, Any]: + """An intermediary relation joining *a* to *b* through the *via* object type. + + ``link_a`` and ``link_b`` name the two M:1 relations it is derived from; + both must be present in the export or the parser rejects it. + """ + relation: dict[str, Any] = { + "rid": f"ri.rel.{key}", + "id": key, + "definition": { + "type": "intermediary", + "intermediary": { + "objectTypeRidA": ot_rid(a), + "objectTypeRidB": ot_rid(b), + "intermediaryObjectTypeRid": ot_rid(via), + "aToIntermediaryLinkTypeRid": f"ri.rel.{link_a}", + "intermediaryToBLinkTypeRid": f"ri.rel.{link_b}", + }, + }, + } + if status: + relation["status"] = {"type": status} + return relation + +def dataset(key: str, columns: Iterable[str]) -> dict[str, Any]: + """The dataset backing object type *key*, with the columns named.""" + return { + "mainDatasetId": ot_rid(key), + "datasetName": f"{key}_table", + "datasetSchema": [{"name": column, "type": "STRING"} for column in columns], + } + +def ot_rid(key: str) -> str: + return f"ri.ot.{key}" + diff --git a/converters/ontology/tests/palantir/converter/helpers.py b/converters/ontology/tests/palantir/converter/helpers.py new file mode 100644 index 00000000..909e9b3c --- /dev/null +++ b/converters/ontology/tests/palantir/converter/helpers.py @@ -0,0 +1,122 @@ +# 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. + +"""Converting a built export, and reading the result back. + +Split out because every suite in this package needs them: `convert` runs an +export through the real parser and converter, and the rest answer questions +about the model that comes out. +""" + +from __future__ import annotations + +import json +from pathlib import Path +from typing import Any, Iterable + + +from ossie_ontology.converter.palantir_to_ossie.converter import PalantirToOssieConverter +from ossie_ontology.vendor.palantir.parser import PalantirParser +from ossie_ontology.model import ConceptMapping, DatasetField, OssieOntology + +from tests.palantir.converter.builders import many_to_one, object_type, prop + + +def _convert( + tmp_path: Path, + object_types: Iterable[dict[str, Any]], + *, + relations: Iterable[dict[str, Any]] = (), + datasets: Iterable[dict[str, Any]] = (), + converter: type[PalantirToOssieConverter] = PalantirToOssieConverter, +) -> OssieOntology: + """Write an extracted-folder export, parse it, and convert it. + + Both files are rewritten on every call, so a test that converts the same + export under two different policies can call this twice. + """ + export = tmp_path / "export" + (export / "data_sets").mkdir(parents=True, exist_ok=True) + (export / "ontology.json").write_text( + json.dumps({"objectTypes": list(object_types), "relations": list(relations)}) + ) + (export / "data_sets" / "ds.json").write_text(json.dumps(list(datasets))) + return converter().convert(PalantirParser().parse(export)) + +def _concept_names(model: OssieOntology) -> set[str]: + return {c.name for c in model.ontology.concepts(exclude_builtin=True)} + +def _relationship_names(model: OssieOntology) -> set[str]: + return {r.full_name for r in model.ontology.relationships} + +def _concept_mapping(model: OssieOntology, concept_name: str) -> ConceptMapping: + mappings = [ + cm + for om in model.ontology_mappings + for cm in om.concept_mappings + if cm.concept.name == concept_name + ] + assert len(mappings) == 1, f"expected one mapping for '{concept_name}', got {len(mappings)}" + return mappings[0] + +def _mapped_relationship_names(model: OssieOntology, concept_name: str) -> set[str]: + """Names of the relationships a concept's mapping actually populates.""" + names = set() + for lm in _concept_mapping(model, concept_name).link_mappings: + for child in lm.children or []: + assert child.relationship is not None + names.add(child.relationship.full_name) + return names + +def _identifier_columns(model: OssieOntology, concept_name: str) -> list[str]: + """The dataset fields a concept's mapping identifies its instances by.""" + [object_mapping] = _concept_mapping(model, concept_name).object_mappings + columns = [] + for rm in object_mapping.referent_mappings or []: + assert isinstance(rm.expression, DatasetField) + columns.append(rm.expression.name) + return columns + +def _linked_export( + *, ot_status: str, relation_status: str +) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]: + """Two object types and the M:1 relation between them, at the given statuses.""" + object_types = [ + object_type("widget", "Widget", status=ot_status), + object_type( + "gadget", + "Gadget", + status=ot_status, + properties=[prop("ri.p.gadget_widget_id", "gadget_widget_id", column="widget_id")], + ), + ] + relations = [ + many_to_one( + "gadget_widget", + one="widget", + many="gadget", + key_map={"ri.p.widget_id": "ri.p.gadget_widget_id"}, + status=relation_status, + ) + ] + return object_types, relations + +def _supertype_names(model: OssieOntology, concept_name: str) -> list[str]: + concept = model.ontology.lookup_concept(concept_name) + assert concept is not None, f"concept '{concept_name}' was not converted" + return [parent.name for parent in concept.extends] + diff --git a/converters/ontology/tests/palantir/converter/test_inheritance.py b/converters/ontology/tests/palantir/converter/test_inheritance.py new file mode 100644 index 00000000..556cba54 --- /dev/null +++ b/converters/ontology/tests/palantir/converter/test_inheritance.py @@ -0,0 +1,245 @@ +# 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. + +"""When a primary-key-to-primary-key relation is read as a subtype edge.""" + +from __future__ import annotations + +from pathlib import Path +from typing import Any + +import pytest + +from ossie_ontology.converter.palantir_to_ossie.converter import PalantirToOssieConverter +from ossie_ontology.vendor.palantir.model import Status +from ossie_ontology.model import OssieOntology + +from tests.palantir.converter.builders import ( + dataset, + many_to_one, + object_type, + prop, +) + +from tests.palantir.converter.helpers import ( + _convert, + _relationship_names, + _identifier_columns, + _linked_export, + _supertype_names, +) + + +def _subtype_export( + *, ot_status: str, relation_status: str +) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]: + """Gadget as a subtype of Widget, at the given statuses.""" + object_types = [ + object_type("widget", "Widget", status=ot_status), + object_type("gadget", "Gadget", status=ot_status), + ] + relations = [ + many_to_one( + "gadget_widget", + one="gadget", + many="widget", + key_map={"ri.p.gadget_id": "ri.p.widget_id"}, + status=relation_status, + ) + ] + return object_types, relations + +def _supertype_names(model: OssieOntology, concept_name: str) -> list[str]: + concept = model.ontology.lookup_concept(concept_name) + assert concept is not None, f"concept '{concept_name}' was not converted" + return [parent.name for parent in concept.extends] + +def test_primary_key_to_primary_key_relation_becomes_inheritance(tmp_path: Path): + object_types, relations = _subtype_export(ot_status="active", relation_status="active") + model = _convert(tmp_path, object_types, relations=relations) + + assert _supertype_names(model, "Gadget") == ["Widget"] + # And only as inheritance: the relation is consumed, not also emitted as a + # reference from the supertype to the subtype. + assert "Widget.gadget_widget" not in _relationship_names(model) + +def test_cycle_broken_subtype_relation_stays_an_ordinary_relationship(tmp_path: Path): + """The other half of the same rule: only inheritance that was actually + applied suppresses the relation. + + Two object types each declared a subtype of the other cannot both extend + the other, so the topological sort drops one edge. That relation is not + carried by any `extends`, so it has to come through as a reference — the + skip in `_convert_relationships` is keyed on what was consumed, not on + everything `_subtype_relations()` matched. + """ + object_types = [ + object_type("widget", "Widget", status="active"), + object_type("gadget", "Gadget", status="active"), + ] + relations = [ + many_to_one( + "gadget_widget", + one="gadget", + many="widget", + key_map={"ri.p.gadget_id": "ri.p.widget_id"}, + status="active", + ), + many_to_one( + "widget_gadget", + one="widget", + many="gadget", + key_map={"ri.p.widget_id": "ri.p.gadget_id"}, + status="active", + ), + ] + with pytest.warns(UserWarning, match="Cycle detected"): + model = _convert(tmp_path, object_types, relations=relations) + + assert _supertype_names(model, "Gadget") == ["Widget"] + assert _supertype_names(model, "Widget") == [] + names = _relationship_names(model) + assert "Widget.gadget_widget" not in names # consumed as the extends above + assert "Gadget.widget_gadget" in names # the dropped edge, as a reference + +def test_relation_that_does_not_map_primary_keys_is_not_inheritance(tmp_path: Path): + """The structural half of the rule: a plain foreign key stays a reference.""" + object_types, relations = _linked_export(ot_status="active", relation_status="active") + model = _convert(tmp_path, object_types, relations=relations) + + assert _supertype_names(model, "Gadget") == [] + assert "Gadget.gadget_widget" in _relationship_names(model) + +def test_partially_mapped_composite_key_is_not_inheritance(tmp_path: Path): + """Inheritance takes the parent's key whole, not in part. + + Widget is identified by (widget_id, wb) and the relation maps only + widget_id, so a Gadget cannot be identified as a Widget — that is an + ordinary foreign key. Reading it as inheritance instead reached + `_convert_mappings`, which resolves every parent primary key through the + property map, and died there on the unmapped `wb`. + """ + object_types = [ + object_type( + "widget", + "Widget", + status="active", + properties=[prop("ri.p.wb", "wb")], + primary_keys=["widget_id", "wb"], + ), + object_type("gadget", "Gadget", status="active"), + ] + relations = [ + many_to_one( + "gadget_widget", + one="gadget", + many="widget", + key_map={"ri.p.gadget_id": "ri.p.widget_id"}, + status="active", + ) + ] + datasets = [dataset("widget", ["widget_id", "wb"]), dataset("gadget", ["gadget_id"])] + + model = _convert(tmp_path, object_types, relations=relations, datasets=datasets) + + assert _supertype_names(model, "Gadget") == [] + assert "Widget.gadget_widget" in _relationship_names(model) + # Gadget keeps its own identity, and its mapping is built rather than crashing. + assert _identifier_columns(model, "Gadget") == ["gadget_id"] + +def test_supertype_excluded_by_the_object_type_policy_is_reported(tmp_path: Path): + """The two status policies are independent, so they can disagree. + + A DEPRECATED parent joined to an ACTIVE child by an ACTIVE PK-to-PK relation + passes the subtype-relation gate and fails the object-type gate under the + defaults, so there is no parent concept to extend. That has to say which + policies disagree — as an error, not an assert, so `-O` fails here too + instead of building `Concept(extends=[None])` and dying later on a `None`. + """ + object_types = [ + object_type("widget", "Widget", status="deprecated"), + object_type("gadget", "Gadget", status="active"), + ] + relations = [ + many_to_one( + "gadget_widget", + one="gadget", + many="widget", + key_map={"ri.p.gadget_id": "ri.p.widget_id"}, + status="active", + ) + ] + + with pytest.raises(ValueError, match="subtype of 'Widget'"): + _convert(tmp_path, object_types, relations=relations) + +def test_deprecated_subtype_relation_is_not_read_as_inheritance(tmp_path: Path): + object_types, relations = _subtype_export(ot_status="active", relation_status="deprecated") + model = _convert(tmp_path, object_types, relations=relations) + + assert _supertype_names(model, "Gadget") == [] + +def test_experimental_subtype_relation_between_active_object_types_is_read(tmp_path: Path): + """Same endpoint rule as any other experimental relation.""" + object_types, relations = _subtype_export(ot_status="active", relation_status="experimental") + model = _convert(tmp_path, object_types, relations=relations) + + assert _supertype_names(model, "Gadget") == ["Widget"] + +def test_subtype_statuses_follow_the_converter_policy(tmp_path: Path): + """Inheritance keeps step with what the converter admits as a concept. + + Widening object types alone leaves an all-experimental hierarchy flat, for + the same reason it leaves experimental relations behind: the endpoints are + checked separately. + """ + + class WidenedObjectTypes(PalantirToOssieConverter): + OBJECT_TYPE_STATUSES = PalantirToOssieConverter.OBJECT_TYPE_STATUSES | {Status.EXPERIMENTAL} + + class WidenedEndpoints(WidenedObjectTypes): + RELATION_ENDPOINT_STATUSES = ( + PalantirToOssieConverter.RELATION_ENDPOINT_STATUSES | {Status.EXPERIMENTAL} + ) + + object_types, relations = _subtype_export( + ot_status="experimental", relation_status="experimental" + ) + + partial = _convert(tmp_path, object_types, relations=relations, converter=WidenedObjectTypes) + assert _supertype_names(partial, "Gadget") == [] + + full = _convert(tmp_path, object_types, relations=relations, converter=WidenedEndpoints) + assert _supertype_names(full, "Gadget") == ["Widget"] + +def test_subtype_relation_statuses_can_be_widened_on_their_own(tmp_path: Path): + """The narrower default is a policy, not a fact about the export.""" + + class IntermediarySubtypes(PalantirToOssieConverter): + SUBTYPE_RELATION_STATUSES = ( + PalantirToOssieConverter.SUBTYPE_RELATION_STATUSES | {Status.INTERMEDIARY} + ) + + object_types, relations = _subtype_export(ot_status="active", relation_status="intermediary") + + assert _supertype_names(_convert(tmp_path, object_types, relations=relations), "Gadget") == [] + + widened = _convert( + tmp_path, object_types, relations=relations, converter=IntermediarySubtypes + ) + assert _supertype_names(widened, "Gadget") == ["Widget"] + diff --git a/converters/ontology/tests/palantir/converter/test_many_to_many.py b/converters/ontology/tests/palantir/converter/test_many_to_many.py new file mode 100644 index 00000000..edf54bdf --- /dev/null +++ b/converters/ontology/tests/palantir/converter/test_many_to_many.py @@ -0,0 +1,133 @@ +# 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. + +"""M:M and intermediary relations, and the derivations they produce.""" + +from __future__ import annotations + +from pathlib import Path +from typing import Any, Iterable + +import pytest + +from ossie_ontology.model import OssieOntology + +from tests.palantir.converter.builders import ( + intermediary, + many_to_many, + many_to_one, + object_type, + prop, +) + +from tests.palantir.converter.helpers import _convert, _relationship_names, _supertype_names + + +def _tagging_export(*, widget_properties: Iterable[dict[str, Any]] = ()): + """Widget tagged with Tag through the WTag join object type. + + The two M:1 links run from WTag, which holds a foreign key to each side — + the ordinary join-table shape, and non-primary-key columns, so neither link + is read as inheritance. + """ + object_types = [ + object_type("widget", "Widget", status="active", properties=list(widget_properties)), + object_type("tag", "Tag", status="active"), + object_type( + "wtag", + "WTag", + status="active", + properties=[ + prop("ri.p.wtag_widget", "wtag_widget"), + prop("ri.p.wtag_tag", "wtag_tag"), + ], + ), + ] + relations = [ + many_to_one( + "a_link", one="widget", many="wtag", + key_map={"ri.p.widget_id": "ri.p.wtag_widget"}, status="active", + ), + many_to_one( + "b_link", one="tag", many="wtag", + key_map={"ri.p.tag_id": "ri.p.wtag_tag"}, status="active", + ), + intermediary( + "tagged", a="widget", b="tag", via="wtag", + link_a="a_link", link_b="b_link", status="active", + ), + ] + return object_types, relations + +def _derived_by_expressions(model: OssieOntology, full_name: str) -> list[str]: + [relationship] = [r for r in model.ontology.relationships if r.full_name == full_name] + return [f.raw_expr for f in relationship.derived_by] + +def test_many_to_many_relation_becomes_a_relationship_on_role_a(tmp_path: Path): + object_types = [ + object_type("widget", "Widget", status="active"), + object_type("tag", "Tag", status="active"), + ] + relations = [many_to_many("tags", a="widget", b="tag", status="active")] + + model = _convert(tmp_path, object_types, relations=relations) + + assert "Widget.tags" in _relationship_names(model) + +def test_intermediary_relation_is_derived_from_its_two_links(tmp_path: Path): + """The relationship lands on role A and is derived by joining both links.""" + object_types, relations = _tagging_export() + + model = _convert(tmp_path, object_types, relations=relations) + + names = _relationship_names(model) + assert "Widget.tagged" in names + assert {"WTag.a_link", "WTag.b_link"} <= names + assert _supertype_names(model, "WTag") == [] + assert _derived_by_expressions(model, "Widget.tagged") == [ + "WTag.a_link(Widget) AND WTag.b_link(Tag)" + ] + +def test_many_to_many_link_colliding_with_a_property_name_is_skipped(tmp_path: Path): + """Same guard as the M:1 case, on the other relation kind.""" + object_types = [ + object_type( + "widget", "Widget", status="active", + properties=[prop("ri.p.tags", "tags", column="tags")], + ), + object_type("tag", "Tag", status="active"), + ] + relations = [many_to_many("tags", a="widget", b="tag", status="active")] + + with pytest.warns(UserWarning, match="Relation 'Widget.tags' collides"): + model = _convert(tmp_path, object_types, relations=relations) + + assert _relationship_names(model) == {"Widget.widget_id", "Widget.tags", "Tag.tag_id"} + +def test_intermediary_link_colliding_with_a_property_name_is_skipped(tmp_path: Path): + """Skipping leaves no half-built relationship: the join formula goes too.""" + object_types, relations = _tagging_export( + widget_properties=[prop("ri.p.tagged", "tagged", column="tagged")] + ) + + with pytest.warns(UserWarning, match="Relation 'Widget.tagged' collides"): + model = _convert(tmp_path, object_types, relations=relations) + + # The surviving 'Widget.tagged' is the property, so it carries no join. + assert _derived_by_expressions(model, "Widget.tagged") == [] + assert {"WTag.a_link", "WTag.b_link"} <= _relationship_names(model) + diff --git a/converters/ontology/tests/palantir/converter/test_multi_dataset_links.py b/converters/ontology/tests/palantir/converter/test_multi_dataset_links.py new file mode 100644 index 00000000..799b4978 --- /dev/null +++ b/converters/ontology/tests/palantir/converter/test_multi_dataset_links.py @@ -0,0 +1,126 @@ +# 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. + +"""Which dataset's mapping a link is attached to when an object type has several.""" + +from __future__ import annotations + +from pathlib import Path +from typing import Any + +import pytest + +from ossie_ontology.model import DatasetField, OssieOntology + +from tests.palantir.converter.builders import ( + dataset, + many_to_one, + object_type, + prop, +) + +from tests.palantir.converter.helpers import _convert + + +def _two_dataset_export(*, second_carries_primary_key: bool): + """Gadget backed by two datasets, each holding the FK to Widget. + + With *second_carries_primary_key* false, nothing in the second dataset + identifies a Gadget, so its ConceptMapping is dropped while the dataset — + and its copy of the foreign-key column — remains. + """ + object_types = [ + object_type("widget", "Widget", status="active"), + object_type( + "gadget", + "Gadget", + status="active", + properties=[prop("ri.p.widget_ref", "widget_ref", column="widget_ref")], + datasources=["ri.ds.a", "ri.ds.b"], + ), + ] + relations = [ + many_to_one( + "gadget_widget", + one="widget", + many="gadget", + key_map={"ri.p.widget_id": "ri.p.widget_ref"}, + status="active", + ) + ] + second_columns = ["gadget_id", "widget_ref"] if second_carries_primary_key else ["widget_ref"] + datasets = [ + { + "mainDatasetId": "ri.ds.a", + "datasetName": "ds_a", + "datasetSchema": [{"name": c, "type": "STRING"} for c in ("gadget_id", "widget_ref")], + }, + { + "mainDatasetId": "ri.ds.b", + "datasetName": "ds_b", + "datasetSchema": [{"name": c, "type": "STRING"} for c in second_columns], + }, + dataset("widget", ["widget_id"]), + ] + return object_types, relations, datasets + +def _link_dataset_pairs(model: OssieOntology, concept_name: str) -> set[tuple[str, str]]: + """For every attached link: (dataset identifying the mapping, dataset the + link's own expression comes from). The two must always be the same one.""" + def dataset_name(expression: Any) -> str: + assert isinstance(expression, DatasetField) + assert expression.dataset is not None + return expression.dataset.name + + pairs: set[tuple[str, str]] = set() + for om in model.ontology_mappings: + for cm in om.concept_mappings: + if cm.concept.name != concept_name: + continue + [root] = cm.object_mappings + identifying = {dataset_name(rm.expression) for rm in (root.referent_mappings or [])} + for link_mapping in cm.link_mappings: + for child in link_mapping.children or []: + for rm in child.object_mapping.referent_mappings or []: + pairs |= {(name, dataset_name(rm.expression)) for name in identifying} + return pairs + +def test_link_is_attached_to_the_mapping_for_its_owndataset(tmp_path: Path): + """Two datasets, two mappings, and each link reads its own dataset.""" + object_types, relations, datasets = _two_dataset_export(second_carries_primary_key=True) + + model = _convert(tmp_path, object_types, relations=relations, datasets=datasets) + + assert _link_dataset_pairs(model, "Gadget") == { + ("Gadget_ds_a", "Gadget_ds_a"), + ("Gadget_ds_b", "Gadget_ds_b"), + } + +def test_link_is_not_attached_to_a_mapping_for_anotherdataset(tmp_path: Path): + """A dataset whose mapping was dropped must not have its columns rehomed. + + The second dataset keeps the foreign-key column but loses its mapping, and + the link built from that column has nowhere to go — attaching it to the + first dataset's mapping would join two unrelated tables under one identity. + """ + object_types, relations, datasets = _two_dataset_export(second_carries_primary_key=False) + + with pytest.warns(UserWarning, match="cannot attach link 'Gadget.gadget_widget'"): + model = _convert(tmp_path, object_types, relations=relations, datasets=datasets) + + assert _link_dataset_pairs(model, "Gadget") == {("Gadget_ds_a", "Gadget_ds_a")} + diff --git a/converters/ontology/tests/palantir/converter/test_object_type_statuses.py b/converters/ontology/tests/palantir/converter/test_object_type_statuses.py new file mode 100644 index 00000000..6ada313d --- /dev/null +++ b/converters/ontology/tests/palantir/converter/test_object_type_statuses.py @@ -0,0 +1,78 @@ +# 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. + +"""Which object types a conversion admits, and how that policy is widened.""" + +from __future__ import annotations + +from pathlib import Path + +import pytest + +from ossie_ontology.converter.palantir_to_ossie.converter import PalantirToOssieConverter +from ossie_ontology.vendor.palantir.model import Status + +from tests.palantir.converter.builders import dataset, object_type, prop + +from tests.palantir.converter.helpers import _convert, _concept_names + + +def test_default_policy_admits_active_endorsed_andintermediary(tmp_path: Path): + model = _convert(tmp_path, [ + object_type("alpha", "Alpha", status="active"), + object_type("bravo", "Bravo", status="endorsed"), + object_type("charlie", "Charlie", status="intermediary"), + object_type("delta", "Delta", status="experimental"), + object_type("echo", "Echo", status="deprecated"), + ]) + assert _concept_names(model) == {"Alpha", "Bravo", "Charlie"} + +def test_object_type_named_like_a_builtin_is_reported(tmp_path: Path): + """`date` pascal-cases to `Date`, which is a builtin value type. + + Letting it through gives the entity the builtin's name, and every DATE + column in the export is then typed by this object type — so it is reported + as the concept-name collision it is, naming the object type rather than + surfacing later as a complaint about a missing 'date_id' property. + """ + object_types = [ + object_type( + "date", + "date", + status="active", + properties=[prop("ri.p.label", "label", column="label")], + ) + ] + + with pytest.raises(ValueError, match="which is a builtin value type"): + _convert(tmp_path, object_types, datasets=[dataset("date", ["date_id", "label"])]) + +def test_object_type_statuses_can_be_widened_by_a_subclass(tmp_path: Path): + """The extension point a draft-ontology converter is built on.""" + + class DraftConverter(PalantirToOssieConverter): + OBJECT_TYPE_STATUSES = PalantirToOssieConverter.OBJECT_TYPE_STATUSES | {Status.EXPERIMENTAL} + + object_types = [ + object_type("alpha", "Alpha", status="active"), + object_type("delta", "Delta", status="experimental"), + ] + assert _concept_names(_convert(tmp_path, object_types)) == {"Alpha"} + + widened = _convert(tmp_path, object_types, converter=DraftConverter) + assert _concept_names(widened) == {"Alpha", "Delta"} + diff --git a/converters/ontology/tests/palantir/converter/test_primary_keys.py b/converters/ontology/tests/palantir/converter/test_primary_keys.py new file mode 100644 index 00000000..959a848b --- /dev/null +++ b/converters/ontology/tests/palantir/converter/test_primary_keys.py @@ -0,0 +1,108 @@ +# 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. + +"""Matching an export's primary keys against the columns a dataset declares.""" + +from __future__ import annotations + +from pathlib import Path +from typing import Any, Iterable + +import pytest + + +from tests.palantir.converter.builders import ( + dataset, + object_type, + ot_rid, + prop, +) + +from tests.palantir.converter.helpers import _convert, _identifier_columns + + +def _location_export( + *, pk_column: str, dataset_columns: Iterable[str] +) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]: + """A Location whose primary key names *pk_column* in its backing dataset.""" + object_types = [ + { + "rid": ot_rid("location"), + "id": "location", + "displayName": "Location", + "properties": [ + prop("ri.p.locno", "locno", pk_columns={ot_rid("location"): pk_column}) + ], + "primaryKeys": ["locno"], + } + ] + return object_types, [dataset("location", dataset_columns)] + +def test_composite_identifier_column_order_is_stable(tmp_path: Path): + """`primary_keys()` is a set of identity-hashed properties, so iterating it + yields a different order per run — which would reach the emitted YAML + through referent_mappings and make any diff of it noisy. + + Converting the same export repeatedly in one process is what exposes this: + a single conversion always looks fine. + """ + object_types = [ + object_type( + "thing", + "Thing", + status="active", + properties=[prop("ri.p.bee", "bee"), prop("ri.p.aye", "aye")], + primary_keys=["thing_id", "bee", "aye"], + ) + ] + datasets = [dataset("thing", ["thing_id", "bee", "aye"])] + + orderings = { + tuple(_identifier_columns(_convert(tmp_path, object_types, datasets=datasets), "Thing")) + for _ in range(20) + } + + assert orderings == {("aye", "bee", "thing_id")} + +def test_primary_key_column_is_matched_case_insensitively(tmp_path: Path): + """`primaryKeyMapping` names the warehouse column, the dataset the API spelling.""" + object_types, datasets = _location_export(pk_column="LOCNO", dataset_columns=["locno"]) + model = _convert(tmp_path, object_types, datasets=datasets) + + assert _identifier_columns(model, "Location") == ["locno"] + +def test_exact_primary_key_column_match_wins(tmp_path: Path): + """A case-sensitive source that carries both spellings keeps its own.""" + object_types, datasets = _location_export( + pk_column="LOCNO", dataset_columns=["locno", "LOCNO"] + ) + model = _convert(tmp_path, object_types, datasets=datasets) + + assert _identifier_columns(model, "Location") == ["LOCNO"] + +def test_primary_key_column_absent_from_dataset_warns(tmp_path: Path): + """A column that is missing outright is still reported, not folded away.""" + object_types, datasets = _location_export(pk_column="MISSING", dataset_columns=["locno"]) + + with pytest.warns(UserWarning, match="does not contain a field named 'MISSING'"): + model = _convert(tmp_path, object_types, datasets=datasets) + + # Nothing identifies the concept, so the mapping is dropped along with it. + assert not [ + cm for om in model.ontology_mappings for cm in om.concept_mappings + ] + diff --git a/converters/ontology/tests/palantir/converter/test_property_statuses.py b/converters/ontology/tests/palantir/converter/test_property_statuses.py new file mode 100644 index 00000000..0d582197 --- /dev/null +++ b/converters/ontology/tests/palantir/converter/test_property_statuses.py @@ -0,0 +1,83 @@ +# 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. + +"""Which properties are declared, which are mapped, and which are dropped.""" + +from __future__ import annotations + +from pathlib import Path +from typing import Any + + +from ossie_ontology.converter.palantir_to_ossie.converter import PalantirToOssieConverter +from ossie_ontology.vendor.palantir.model import Status + +from tests.palantir.converter.builders import dataset, object_type, prop + +from tests.palantir.converter.helpers import ( + _convert, + _relationship_names, + _mapped_relationship_names, +) + + +def _widget_export() -> tuple[list[dict[str, Any]], list[dict[str, Any]]]: + """One object type with a property per status, and its backing dataset.""" + object_types = [ + object_type( + "widget", + "Widget", + status="active", + properties=[ + prop("ri.p.label", "label", status="active", column="label"), + prop("ri.p.note", "note", status="experimental", column="note"), + prop("ri.p.legacy", "legacy", status="deprecated", column="legacy"), + ], + ) + ] + return object_types, [dataset("widget", ["widget_id", "label", "note", "legacy"])] + +def test_experimental_property_is_declared_but_not_mapped(tmp_path: Path): + """An experimental property is part of the ontology, but nothing populates it. + + The two policies differ on purpose: the relationship is worth declaring + while the work is in progress, but pointing a mapping at a column that is + still being reshaped is not. + """ + object_types, datasets = _widget_export() + model = _convert(tmp_path, object_types, datasets=datasets) + + assert "Widget.note" in _relationship_names(model) + assert _mapped_relationship_names(model, "Widget") == {"Widget.label"} + +def test_deprecated_property_is_not_converted_at_all(tmp_path: Path): + object_types, datasets = _widget_export() + model = _convert(tmp_path, object_types, datasets=datasets) + + assert "Widget.legacy" not in _relationship_names(model) + +def test_mapping_property_statuses_can_be_widened_by_a_subclass(tmp_path: Path): + class DraftConverter(PalantirToOssieConverter): + MAPPING_PROPERTY_STATUSES = ( + PalantirToOssieConverter.MAPPING_PROPERTY_STATUSES | {Status.EXPERIMENTAL} + ) + + object_types, datasets = _widget_export() + model = _convert(tmp_path, object_types, datasets=datasets, converter=DraftConverter) + + assert _mapped_relationship_names(model, "Widget") == {"Widget.label", "Widget.note"} + diff --git a/converters/ontology/tests/palantir/converter/test_property_types.py b/converters/ontology/tests/palantir/converter/test_property_types.py new file mode 100644 index 00000000..2de108b9 --- /dev/null +++ b/converters/ontology/tests/palantir/converter/test_property_types.py @@ -0,0 +1,269 @@ +# 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. + +"""How an export's column types become concepts, and how names survive.""" + +from __future__ import annotations + +from pathlib import Path + +import pytest + +from ossie_ontology.converter.ossie_to_spec.converter import OssieToSpecConverter +from ossie_ontology.model import DatasetField, OssieOntology +from ossie_ontology.parser import OssieParser + +from tests.palantir.converter.builders import ( + dataset, + object_type, + ot_rid, + prop, +) + +from tests.palantir.converter.helpers import ( + _convert, + _mapped_relationship_names, + _identifier_columns, +) + + +def _role_player_types(model: OssieOntology) -> dict[str, str]: + """The concept each relationship's value role is typed by.""" + return {r.full_name: r.signature[-1].name for r in model.ontology.relationships} + +def _field_types(model: OssieOntology) -> dict[str, str]: + """The concept each dataset field is typed by.""" + return { + field.name: field.type.name + for om in model.ontology_mappings + for dataset in om.semantic_model.datasets + for field in dataset.fields + if field.type is not None + } + +def test_property_types_without_a_scalar_equivalent_are_not_numeric(tmp_path: Path): + """A blob or a reference must not come out typed as a number. + + Both the madlib role and the dataset field are typed from the same mapping, + so a wrong answer asserts arithmetic over an attachment column in two + places at once, with nothing downstream to reject it. + """ + types = { + "attachment": "ATTACHMENT", + "media_ref": "MEDIA_REFERENCE", + "secret": "CIPHER_TEXT", + "shape": "STRUCT", + "embedding": "VECTOR", + "whatever": "ANY", + "big": "LONG", + "small": "SHORT", + } + object_types = [ + object_type( + "doc", + "Doc", + status="active", + properties=[ + prop(f"ri.p.{name}", name, column=name, type_name=palantir_type) + for name, palantir_type in types.items() + ], + ) + ] + datasets = [ + { + "mainDatasetId": ot_rid("doc"), + "datasetName": "doc_table", + "datasetSchema": [{"name": "doc_id", "type": "STRING"}] + + [{"name": name, "type": palantir_type} for name, palantir_type in types.items()], + } + ] + + model = _convert(tmp_path, object_types, datasets=datasets) + + expected = { + "attachment": "String", # an attachment rid + "media_ref": "String", # a media reference + "secret": "String", # ciphertext + "shape": "String", # no builtin describes a struct + "embedding": "String", # nor a vector + "whatever": "Any", # this one has an exact builtin + "big": "Integer", + "small": "Integer", + } + role_types = _role_player_types(model) + assert {name: role_types[f"Doc.{name}"] for name in expected} == expected + field_types = _field_types(model) + assert {name: field_types[name] for name in expected} == expected + +def test_unrecognized_column_type_falls_back_to_string(tmp_path: Path): + """A warehouse type the enum has no name for must not fail the conversion. + + Column types drift with the export format, the field type is not serialized + downstream at all, and a column with no type already falls back to String — + so an unknown one is reported and carried, not raised. A bare ARRAY column + is still dropped, as before. + """ + object_types = [ + object_type( + "doc", + "Doc", + status="active", + properties=[prop("ri.p.blob", "blob", column="blob")], + ) + ] + datasets = [ + { + "mainDatasetId": ot_rid("doc"), + "datasetName": "doc_table", + "datasetSchema": [ + {"name": "doc_id", "type": "STRING"}, + {"name": "blob", "type": "BINARY"}, + {"name": "props", "type": "MAP"}, + {"name": "tags", "type": "ARRAY"}, + {"name": "untyped"}, + ], + } + ] + + with pytest.warns(UserWarning, match="Unrecognized column type 'BINARY'"): + model = _convert(tmp_path, object_types, datasets=datasets) + + assert _field_types(model) == { + "doc_id": "String", + "blob": "String", + "props": "String", + "untyped": "String", # the pre-existing fallback this one now matches + } + # And the mapping is still built, rather than lost with the run. + assert _identifier_columns(model, "Doc") == ["doc_id"] + +def test_column_name_becomes_an_identifier_and_keeps_reading_its_column(tmp_path: Path): + """A column name that is not an identifier has to be handled on both sides. + + The field *name* is what a mapping expression refers to, and it is matched + against an identifier pattern when the spec is read back — a name holding a + space fails that and is quietly taken for a formula rather than the field. + The field's *expression* is the SQL that reads the column, so it has to go + on naming the column exactly, quoted. + """ + columns = {"net amt": "net_amt", "a-b": "a_b", "x.y": "x_y", "9lives": "_9lives"} + object_types = [ + object_type( + "doc", + "Doc", + status="active", + properties=[ + prop(f"ri.p.p{i}", f"p{i}", column=column) + for i, column in enumerate(columns) + ], + ) + ] + datasets = [ + { + "mainDatasetId": ot_rid("doc"), + "datasetName": "doc_table", + "datasetSchema": [{"name": "doc_id", "type": "STRING"}] + + [{"name": column, "type": "STRING"} for column in columns], + } + ] + + model = _convert(tmp_path, object_types, datasets=datasets) + + [dataset] = [ds for om in model.ontology_mappings for ds in om.semantic_model.datasets] + expressions = { + field.name: field.expression.dialects[0].expression + for field in dataset.fields + if field.expression is not None + } + assert set(expressions) == {"doc_id", *columns.values()} + assert expressions == { + "doc_id": "doc_id", # already an identifier, so left alone + "net_amt": '"net amt"', + "a_b": '"a-b"', + "x_y": '"x.y"', + "_9lives": '"9lives"', # a leading digit is not a bare identifier either + } + +def test_field_name_survives_a_round_trip_as_a_field(tmp_path: Path): + """The reason the name matters: it has to resolve on the way back in.""" + object_types = [ + object_type( + "doc", + "Doc", + status="active", + properties=[prop("ri.p.net_amt", "net_amt", column="net amt")], + ) + ] + datasets = [ + { + "mainDatasetId": ot_rid("doc"), + "datasetName": "doc_table", + "datasetSchema": [ + {"name": "doc_id", "type": "STRING"}, + {"name": "net amt", "type": "STRING"}, + ], + } + ] + model = _convert(tmp_path, object_types, datasets=datasets) + + roundtrip = tmp_path / "roundtrip.yaml" + roundtrip.write_text(OssieToSpecConverter.convert(model).dump_yaml()) + reparsed = OssieParser().parse(roundtrip) + + expressions = [ + child.object_mapping.expression + for om in reparsed.ontology_mappings + for cm in om.concept_mappings + for link_mapping in cm.link_mappings + for child in link_mapping.children or [] + ] + assert expressions, "the property mapping did not survive the round trip" + for expression in expressions: + assert isinstance(expression, DatasetField), f"demoted to {type(expression).__name__}" + +def test_column_without_a_name_does_not_reach_the_converter(tmp_path: Path): + """The parser drops it; conversion proceeds on the columns that remain. + + This used to crash in `_normalize_field_name` on a `None` name, naming + neither the dataset nor the column. + """ + object_types = [ + object_type( + "doc", + "Doc", + status="active", + properties=[prop("ri.p.label", "label", column="label")], + ) + ] + datasets = [ + { + "mainDatasetId": ot_rid("doc"), + "datasetName": "doc_table", + "datasetSchema": [ + {"name": "doc_id", "type": "STRING"}, + {"type": "STRING"}, + {"name": "label", "type": "STRING"}, + ], + } + ] + + with pytest.warns(UserWarning, match="Skipping column with missing name"): + model = _convert(tmp_path, object_types, datasets=datasets) + + assert set(_field_types(model)) == {"doc_id", "label"} + assert _mapped_relationship_names(model, "Doc") == {"Doc.label"} + diff --git a/converters/ontology/tests/palantir/converter/test_relation_statuses.py b/converters/ontology/tests/palantir/converter/test_relation_statuses.py new file mode 100644 index 00000000..4bee3ec5 --- /dev/null +++ b/converters/ontology/tests/palantir/converter/test_relation_statuses.py @@ -0,0 +1,145 @@ +# 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. + +"""Which relations become relationships, and what blocks one.""" + +from __future__ import annotations + +from pathlib import Path +from typing import Any + +import pytest + +from ossie_ontology.converter.palantir_to_ossie.converter import PalantirToOssieConverter +from ossie_ontology.vendor.palantir.model import Status + +from tests.palantir.converter.builders import ( + dataset, + many_to_one, + object_type, + prop, +) + +from tests.palantir.converter.helpers import ( + _convert, + _concept_names, + _relationship_names, + _mapped_relationship_names, + _linked_export, +) + + +def _linked_export( + *, ot_status: str, relation_status: str +) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]: + """Two object types and the M:1 relation between them, at the given statuses.""" + object_types = [ + object_type("widget", "Widget", status=ot_status), + object_type( + "gadget", + "Gadget", + status=ot_status, + properties=[prop("ri.p.gadget_widget_id", "gadget_widget_id", column="widget_id")], + ), + ] + relations = [ + many_to_one( + "gadget_widget", + one="widget", + many="gadget", + key_map={"ri.p.widget_id": "ri.p.gadget_widget_id"}, + status=relation_status, + ) + ] + return object_types, relations + +def test_relation_statuses_gate_conversion(tmp_path: Path): + object_types, relations = _linked_export(ot_status="active", relation_status="deprecated") + model = _convert(tmp_path, object_types, relations=relations) + + assert "Gadget.gadget_widget" not in _relationship_names(model) + +def test_experimental_relation_between_active_object_types_is_converted(tmp_path: Path): + """Experimental relations ride in on their endpoints rather than their own status.""" + object_types, relations = _linked_export(ot_status="active", relation_status="experimental") + model = _convert(tmp_path, object_types, relations=relations) + + assert "Gadget.gadget_widget" in _relationship_names(model) + +def test_experimental_relation_needs_its_endpoints_admitted_too(tmp_path: Path): + """Widening object types alone leaves their experimental relations behind. + + The endpoint check is separate from the object-type policy, so a converter + that admits experimental object types still drops every experimental + relation between them until it widens the endpoint statuses as well. + """ + + class WidenedObjectTypes(PalantirToOssieConverter): + OBJECT_TYPE_STATUSES = PalantirToOssieConverter.OBJECT_TYPE_STATUSES | {Status.EXPERIMENTAL} + + class WidenedEndpoints(WidenedObjectTypes): + RELATION_ENDPOINT_STATUSES = ( + PalantirToOssieConverter.RELATION_ENDPOINT_STATUSES | {Status.EXPERIMENTAL} + ) + + object_types, relations = _linked_export( + ot_status="experimental", relation_status="experimental" + ) + + partial = _convert(tmp_path, object_types, relations=relations, converter=WidenedObjectTypes) + assert _concept_names(partial) == {"Widget", "Gadget"} + assert "Gadget.gadget_widget" not in _relationship_names(partial) + + full = _convert(tmp_path, object_types, relations=relations, converter=WidenedEndpoints) + assert "Gadget.gadget_widget" in _relationship_names(full) + +def test_link_colliding_with_a_property_name_is_skipped(tmp_path: Path): + """A shared name costs the link, not the conversion. + + Exports routinely name a link after the foreign-key column backing it, and + both become relationships on the same concept — which the ontology rejects + as a duplicate. The relation is dropped with a warning; the property, which + is converted first, keeps the name. + """ + object_types = [ + object_type("widget", "Widget", status="active"), + object_type( + "gadget", + "Gadget", + status="active", + properties=[prop("ri.p.owner", "owner", column="owner")], + ), + ] + relations = [ + many_to_one( + "owner", + one="widget", + many="gadget", + key_map={"ri.p.widget_id": "ri.p.owner"}, + status="active", + ) + ] + datasets = [dataset("widget", ["widget_id"]), dataset("gadget", ["gadget_id", "owner"])] + + with pytest.warns(UserWarning, match="Relation 'Gadget.owner' collides"): + model = _convert(tmp_path, object_types, relations=relations, datasets=datasets) + + # One 'owner' relationship, and it is the property's — still mapped to its + # own column, rather than the skipped link's walk into Widget. + assert _relationship_names(model) == {"Widget.widget_id", "Gadget.gadget_id", "Gadget.owner"} + assert _mapped_relationship_names(model, "Gadget") == {"Gadget.owner"} + diff --git a/converters/ontology/tests/test_palantir_parser.py b/converters/ontology/tests/palantir/test_parser.py similarity index 98% rename from converters/ontology/tests/test_palantir_parser.py rename to converters/ontology/tests/palantir/test_parser.py index 4383e414..1dfee35e 100644 --- a/converters/ontology/tests/test_palantir_parser.py +++ b/converters/ontology/tests/palantir/test_parser.py @@ -36,8 +36,8 @@ import pytest -from ossie_ontology.external.palantir.model import DataType, Ontology -from ossie_ontology.external.palantir.parser import PalantirOntologyParser, PalantirParser +from ossie_ontology.vendor.palantir.model import DataType, Ontology +from ossie_ontology.vendor.palantir.parser import PalantirOntologyParser, PalantirParser from ossie_ontology.model import BUILTIN_CONCEPTS # A minimal-but-complete Palantir export: one object type backed by one dataset. diff --git a/converters/ontology/tests/relationalai/__init__.py b/converters/ontology/tests/relationalai/__init__.py new file mode 100644 index 00000000..dbfc45f5 --- /dev/null +++ b/converters/ontology/tests/relationalai/__init__.py @@ -0,0 +1,24 @@ +# 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. + +"""The RelationalAI spoke: converting an Ossie ontology into a PyRel model. + +Nothing here talks to an engine. Conversion and compilation happen in-process, +and `snapshots/` holds the PyRel source each fixture decompiles to — the code a +user would otherwise have hand-written, so a converter change reads as a +reviewable diff. +""" diff --git a/converters/ontology/tests/relationalai/snapshots/test_converter/test_converted_model_decompiles_to_expected_pyrel/flights/flights.py b/converters/ontology/tests/relationalai/snapshots/test_converter/test_converted_model_decompiles_to_expected_pyrel/flights/flights.py new file mode 100644 index 00000000..d489a3e4 --- /dev/null +++ b/converters/ontology/tests/relationalai/snapshots/test_converter/test_converted_model_decompiles_to_expected_pyrel/flights/flights.py @@ -0,0 +1,199 @@ +# 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 relationalai.semantics import Date, DateTime, Decimal, Integer, Model, String, avg + +m = Model("model") + +NrFeet = m.Concept("NrFeet", extends=[Decimal]) +RunwayLength = m.Concept("RunwayLength", extends=[NrFeet]) +NrPounds = m.Concept("NrPounds", extends=[Integer]) +Capacity = m.Concept("Capacity", extends=[NrPounds]) +NrMiles = m.Concept("NrMiles", extends=[Decimal]) +Distance = m.Concept("Distance", extends=[NrMiles]) +NrMinutes = m.Concept("NrMinutes", extends=[Decimal]) +Delay = m.Concept("Delay", extends=[NrMinutes]) +CancelationCode = m.Concept("CancelationCode", extends=[String]) +DegreesLatitude = m.Concept("DegreesLatitude", extends=[Decimal]) +DegreesLongitude = m.Concept("DegreesLongitude", extends=[Decimal]) +Polygon = m.Concept("Polygon", extends=[String]) +RunwayGeometry = m.Concept("RunwayGeometry", extends=[Polygon]) +CityName = m.Concept("CityName", extends=[String]) +DistanceGroup = m.Concept("DistanceGroup", extends=[Integer]) +RunwayDesignator = m.Concept("RunwayDesignator", extends=[String]) +SerialNr = m.Concept("SerialNr", extends=[String]) +StateCode = m.Concept("StateCode", extends=[String]) +StateName = m.Concept("StateName", extends=[String]) +TailNr = m.Concept("TailNr", extends=[String]) +Year = m.Concept("Year", extends=[String]) +CancelationReason = m.Concept("CancelationReason") +State = m.Concept("State") +City = m.Concept("City") +Runway = m.Concept("Runway") +Airport = m.Concept("Airport") +AirportCode = m.Concept("AirportCode", extends=[String]) +ManufacturerName = m.Concept("ManufacturerName", extends=[String]) +Manufacturer = m.Concept("Manufacturer") +ModelName = m.Concept("ModelName", extends=[String]) +Model_2 = m.Concept("Model") +Aircraft = m.Concept("Aircraft") +AirportName = m.Concept("AirportName", extends=[String]) +AirportId = m.Concept("AirportId", extends=[String]) +MarketName = m.Concept("MarketName", extends=[String]) +Market = m.Concept("Market") +FlightNr = m.Concept("FlightNr", extends=[String]) +FlightId = m.Concept("FlightId", extends=[String]) +Flight = m.Concept("Flight") +CarrierCode = m.Concept("CarrierCode", extends=[String]) +CarrierName = m.Concept("CarrierName", extends=[String]) +Carrier = m.Concept("Carrier") +RouteId = m.Concept("RouteId", extends=[String]) +Route = m.Concept("Route") +database_schema_runways = m.Table("DATABASE.SCHEMA.RUNWAYS", schema={'AIRPORT_CODE': String, 'LENGTH': Decimal, 'SHAPE': String, 'DESIGNATOR': String}) +database_schema_aircraft = m.Table("DATABASE.SCHEMA.AIRCRAFT", schema={'SERIAL_NR': String, 'NAME': String, 'NR_SEATS': Integer, 'TAIL_NR': String, 'CARRIER_CODE': String, 'MANUFACTURER': String, 'MODEL': String, 'YEAR': String, 'CAPACITY': Integer}) +database_schema_airports = m.Table("DATABASE.SCHEMA.AIRPORTS", schema={'STATE_CODE': String, 'MARKET_NM': String, 'LONGITUDE': Decimal, 'OPENED': String, 'STATE_NM': String, 'CITY_NM': String, 'NAME': String, 'CODE': String, 'LATITUDE': Decimal}) +database_schema_flights = m.Table("DATABASE.SCHEMA.FLIGHTS", schema={'DEP_DELAY': Decimal, 'AIR_TIME': String, 'NR': String, 'CARRIER_CODE': String, 'DURATION': String, 'SCHEDULED_DEPARTURE': DateTime, 'ARR_DELAY': Decimal, 'CANCELLED': String, 'CANCEL_CODE': String, 'DISTANCE': Decimal, 'ID': String, 'SCHEDULED_ARRIVAL': DateTime, 'WHEELS_ON': String, 'DATE': Date, 'WHEELS_OFF': String, 'SCHEDULED_DURATION': String, 'TAIL_NR': String, 'DEPARTURE': DateTime, 'ROUTE_ID': String, 'DIVERTED': String, 'ARRIVAL': DateTime}) +database_schema_carriers = m.Table("DATABASE.SCHEMA.CARRIERS", schema={'CODE': String, 'NAME': String}) +database_schema_routes = m.Table("DATABASE.SCHEMA.ROUTES", schema={'ORIG_AIRPORT_CODE': String, 'DEST_AIRPORT_CODE': String, 'DISTANCE': Decimal, 'DIST_GRP': Integer, 'ID': String, 'NAME': String}) + +CancelationReason.code = m.Property(f"{CancelationReason} is identified by {CancelationCode}") +State.code = m.Property(f"{State} is identified by {StateCode}") +City.name = m.Property(f"{City} has {CityName}") +City.state = m.Property(f"{City} is located in {State}") +Runway.designator = m.Property(f"{Runway} uses {RunwayDesignator}") +Airport.code = m.Property(f"{Airport} has {AirportCode}") +Runway.airport = m.Property(f"{Runway} belongs to {Airport}") +Manufacturer.name = m.Property(f"{Manufacturer} is identified by {ManufacturerName}") +Model_2.name = m.Property(f"{Model_2} has {ModelName}") +Model_2.manufacturer = m.Property(f"{Model_2} is manufactured by {Manufacturer}") +Aircraft.tailnum = m.Property(f"{Aircraft} is identified by {TailNr}") +Market.name = m.Property(f"{Market} is identified by {MarketName}") +Flight.id = m.Property(f"{Flight} is identified by {FlightId}") +Carrier.code = m.Property(f"{Carrier} uses {CarrierCode}") +Route.id = m.Property(f"{Route} is identified by {RouteId}") +CancelationReason.description = m.Property(f"{CancelationReason} has description {String}") +State.name = m.Property(f"{State} has {StateName}") +Runway.length = m.Property(f"{Runway} has {RunwayLength}") +Runway.geometry = m.Property(f"{Runway} has {RunwayGeometry}") +Aircraft.serial_number = m.Property(f"{Aircraft} has {SerialNr}") +Aircraft.name = m.Property(f"{Aircraft} has name {String}") +Aircraft.number_of_seats = m.Property(f"{Aircraft} has {Integer} seats") +Aircraft.model = m.Property(f"{Aircraft} has {Model_2}") +Aircraft.year_manufactured = m.Property(f"{Aircraft} was manufactured in {Year}") +Aircraft.capacity = m.Property(f"{Aircraft} has {Capacity}") +Aircraft.carrier = m.Property(f"{Aircraft} is operated by {Carrier}") +Airport.city = m.Property(f"{Airport} is located in {City}") +Airport.serves = m.Property(f"{Airport} serves {Market}") +Airport.average_departure_delay = m.Property(f"{Airport} has average departure {Delay}") +Airport.longitude = m.Property(f"{Airport} centers at {DegreesLongitude}") +Airport.average_arrival_delay = m.Property(f"{Airport} has average arrival {Delay}") +Airport.name = m.Property(f"{Airport} has {AirportName}") +Airport.latitude = m.Property(f"{Airport} centers at {DegreesLatitude}") +Flight.departure_delay = m.Property(f"{Flight} has departure {Delay}") +Flight.number = m.Property(f"{Flight} has {FlightNr}") +Flight.scheduled_departure = m.Property(f"{Flight} is scheduled to depart at {DateTime}") +Flight.arrival_delay = m.Property(f"{Flight} has arrival {Delay}") +Flight.canceled = m.Relationship(f"{Flight} was canceled") +Flight.canceled_due_to = m.Property(f"{Flight} was canceled due to {CancelationReason}") +Flight.distance = m.Property(f"{Flight} spans actual {Distance}") +Flight.scheduled_arrival = m.Property(f"{Flight} is scheduled to arrive at {DateTime}") +Flight.registers_longitude_series = m.Property(f"{Flight} at {DateTime} registers {DegreesLongitude}") +Flight.registers_latitude_series = m.Property(f"{Flight} at {DateTime} registers {DegreesLatitude}") +Flight.date = m.Property(f"{Flight} is scheduled to depart on {Date}") +Flight.departs_at = m.Property(f"{Flight} departs at {DateTime}") +Flight.diverted = m.Relationship(f"{Flight} was diverted") +Flight.arrives_at = m.Property(f"{Flight} arrives at {DateTime}") +Flight.route = m.Property(f"{Flight} traverses {Route}") +Flight.aircraft = m.Property(f"{Flight} uses {Aircraft}") +Flight.operated_by = m.Property(f"{Flight} is operated by {Carrier}") +Carrier.name = m.Property(f"{Carrier} has {CarrierName}") +Route.average_departure_delay = m.Property(f"{Route} has average departure {Delay}") +Route.average_arrival_delay = m.Property(f"{Route} has average arrival {Delay}") +Route.distance = m.Property(f"{Route} spans {Distance}") +Route.lies_in = m.Property(f"{Route} has {DistanceGroup}") +Route.route_name = m.Property(f"{Route} has name {String}") +Route.destination = m.Property(f"{Route} connects to destination {Airport}") +Route.departure = m.Property(f"{Route} connects to departure {Airport}") + +CancelationReason.identify_by(CancelationReason.code) +State.identify_by(State.code) +City.identify_by(City.name) +City.identify_by(City.state) +Runway.identify_by(Runway.designator) +Runway.identify_by(Runway.airport) +Airport.identify_by(Airport.code) +Manufacturer.identify_by(Manufacturer.name) +Model_2.identify_by(Model_2.name) +Model_2.identify_by(Model_2.manufacturer) +Aircraft.identify_by(Aircraft.tailnum) +Market.identify_by(Market.name) +Flight.identify_by(Flight.id) +Carrier.identify_by(Carrier.code) +Route.identify_by(Route.id) +m.where(airport := Airport.lookup(code=database_schema_runways.AIRPORT_CODE)).define(Runway.new(designator=database_schema_runways.DESIGNATOR, airport=airport)) +m.where(runway := Runway.to_identity(designator=database_schema_runways.DESIGNATOR, airport=airport, unsafe=True), airport := Airport.lookup(code=database_schema_runways.AIRPORT_CODE)).define(runway.length(RunwayLength(database_schema_runways.LENGTH))) +m.where(runway := Runway.to_identity(designator=database_schema_runways.DESIGNATOR, airport=airport, unsafe=True), airport := Airport.lookup(code=database_schema_runways.AIRPORT_CODE)).define(runway.geometry(RunwayGeometry(database_schema_runways.SHAPE))) +m.define(Manufacturer.new(name=database_schema_aircraft.MANUFACTURER)) +m.where(manufacturer := Manufacturer.lookup(name=database_schema_aircraft.MANUFACTURER)).define(Model_2.new(name=database_schema_aircraft.MODEL, manufacturer=manufacturer)) +m.define(Aircraft.new(tailnum=database_schema_aircraft.TAIL_NR)) +m.where(aircraft := Aircraft.to_identity(tailnum=database_schema_aircraft.TAIL_NR, unsafe=True)).define(aircraft.serial_number(SerialNr(database_schema_aircraft.SERIAL_NR))) +m.where(aircraft := Aircraft.to_identity(tailnum=database_schema_aircraft.TAIL_NR, unsafe=True)).define(aircraft.name(database_schema_aircraft.NAME)) +m.where(aircraft := Aircraft.to_identity(tailnum=database_schema_aircraft.TAIL_NR, unsafe=True)).define(aircraft.number_of_seats(database_schema_aircraft.NR_SEATS)) +m.where(aircraft := Aircraft.to_identity(tailnum=database_schema_aircraft.TAIL_NR, unsafe=True), manufacturer := Manufacturer.lookup(name=database_schema_aircraft.MANUFACTURER), model := Model_2.lookup(name=database_schema_aircraft.MODEL, manufacturer=manufacturer)).define(aircraft.model(model)) +m.where(aircraft := Aircraft.to_identity(tailnum=database_schema_aircraft.TAIL_NR, unsafe=True)).define(aircraft.year_manufactured(Year(database_schema_aircraft.YEAR))) +m.where(aircraft := Aircraft.to_identity(tailnum=database_schema_aircraft.TAIL_NR, unsafe=True)).define(aircraft.capacity(Capacity(database_schema_aircraft.CAPACITY))) +m.where(aircraft := Aircraft.to_identity(tailnum=database_schema_aircraft.TAIL_NR, unsafe=True), carrier := Carrier.lookup(code=database_schema_aircraft.CARRIER_CODE)).define(aircraft.carrier(carrier)) +m.define(State.new(code=database_schema_airports.STATE_CODE)) +m.where(state := State.to_identity(code=database_schema_airports.STATE_CODE, unsafe=True)).define(state.name(StateName(database_schema_airports.STATE_NM))) +m.where(state := State.lookup(code=database_schema_airports.STATE_CODE)).define(City.new(name=database_schema_airports.CITY_NM, state=state)) +m.define(Market.new(name=database_schema_airports.MARKET_NM)) +m.define(Airport.new(code=database_schema_airports.CODE)) +m.where(airport := Airport.to_identity(code=database_schema_airports.CODE, unsafe=True), state := State.lookup(code=database_schema_airports.STATE_CODE), city := City.lookup(name=database_schema_airports.CITY_NM, state=state)).define(airport.city(city)) +m.where(airport := Airport.to_identity(code=database_schema_airports.CODE, unsafe=True), market := Market.lookup(name=database_schema_airports.MARKET_NM)).define(airport.serves(market)) +m.where(airport := Airport.to_identity(code=database_schema_airports.CODE, unsafe=True)).define(airport.longitude(DegreesLongitude(database_schema_airports.LONGITUDE))) +m.where(airport := Airport.to_identity(code=database_schema_airports.CODE, unsafe=True)).define(airport.name(AirportName(database_schema_airports.NAME))) +m.where(airport := Airport.to_identity(code=database_schema_airports.CODE, unsafe=True)).define(airport.latitude(DegreesLatitude(database_schema_airports.LATITUDE))) +m.define(Flight.new(id=database_schema_flights.ID)) +m.where(database_schema_flights.DIVERTED == True).define(Flight.new(id=database_schema_flights.ID)) +m.where(flight := Flight.to_identity(id=database_schema_flights.ID, unsafe=True), database_schema_flights.DIVERTED == True).define(Flight.diverted(flight)) +m.where(database_schema_flights.CANCELLED == True).define(Flight.new(id=database_schema_flights.ID)) +m.where(flight := Flight.to_identity(id=database_schema_flights.ID, unsafe=True), database_schema_flights.CANCELLED == True).define(Flight.canceled(flight)) +m.where(flight := Flight.to_identity(id=database_schema_flights.ID, unsafe=True)).define(flight.arrival_delay(Delay(database_schema_flights.ARR_DELAY))) +m.where(flight := Flight.to_identity(id=database_schema_flights.ID, unsafe=True)).define(flight.departure_delay(Delay(database_schema_flights.DEP_DELAY))) +m.where(flight := Flight.to_identity(id=database_schema_flights.ID, unsafe=True)).define(flight.number(FlightNr(database_schema_flights.NR))) +m.where(flight := Flight.to_identity(id=database_schema_flights.ID, unsafe=True)).define(flight.scheduled_departure(database_schema_flights.SCHEDULED_DEPARTURE)) +m.where(flight := Flight.to_identity(id=database_schema_flights.ID, unsafe=True), cancelationreason := CancelationReason.lookup(code=database_schema_flights.CANCEL_CODE)).define(flight.canceled_due_to(cancelationreason)) +m.where(flight := Flight.to_identity(id=database_schema_flights.ID, unsafe=True)).define(flight.distance(Distance(database_schema_flights.DISTANCE))) +m.where(flight := Flight.to_identity(id=database_schema_flights.ID, unsafe=True)).define(flight.scheduled_arrival(database_schema_flights.SCHEDULED_ARRIVAL)) +m.where(flight := Flight.to_identity(id=database_schema_flights.ID, unsafe=True)).define(flight.date(database_schema_flights.DATE)) +m.where(flight := Flight.to_identity(id=database_schema_flights.ID, unsafe=True)).define(flight.departs_at(database_schema_flights.DEPARTURE)) +m.where(flight := Flight.to_identity(id=database_schema_flights.ID, unsafe=True)).define(flight.arrives_at(database_schema_flights.ARRIVAL)) +m.where(flight := Flight.to_identity(id=database_schema_flights.ID, unsafe=True), route := Route.lookup(id=database_schema_flights.ROUTE_ID)).define(flight.route(route)) +m.where(flight := Flight.to_identity(id=database_schema_flights.ID, unsafe=True), aircraft := Aircraft.lookup(tailnum=database_schema_flights.TAIL_NR)).define(flight.aircraft(aircraft)) +m.where(flight := Flight.to_identity(id=database_schema_flights.ID, unsafe=True), carrier := Carrier.lookup(code=database_schema_flights.CARRIER_CODE)).define(flight.operated_by(carrier)) +m.define(Carrier.new(code=database_schema_carriers.CODE)) +m.where(carrier := Carrier.to_identity(code=database_schema_carriers.CODE, unsafe=True)).define(carrier.name(CarrierName(database_schema_carriers.NAME))) +m.define(Route.new(id=database_schema_routes.ID)) +m.where(route := Route.to_identity(id=database_schema_routes.ID, unsafe=True)).define(route.distance(Distance(database_schema_routes.DISTANCE))) +m.where(route := Route.to_identity(id=database_schema_routes.ID, unsafe=True)).define(route.lies_in(DistanceGroup(database_schema_routes.DIST_GRP))) +m.where(route := Route.to_identity(id=database_schema_routes.ID, unsafe=True)).define(route.route_name(database_schema_routes.NAME)) +m.where(route := Route.to_identity(id=database_schema_routes.ID, unsafe=True), airport := Airport.lookup(code=database_schema_routes.DEST_AIRPORT_CODE)).define(route.destination(airport)) +m.where(route := Route.to_identity(id=database_schema_routes.ID, unsafe=True), airport := Airport.lookup(code=database_schema_routes.ORIG_AIRPORT_CODE)).define(route.departure(airport)) +m.where(delay := Delay.ref(), Airport, delay == avg(Flight.departure_delay).per(Airport).where(Airport == Flight.route.departure)).define(Airport.average_departure_delay(delay)) +m.where(delay := Delay.ref(), Airport, delay == avg(Flight.arrival_delay).per(Airport).where(Airport == Flight.route.destination)).define(Airport.average_arrival_delay(delay)) +m.where(delay := Delay.ref(), Route, delay == avg(Flight.departure_delay).per(Route).where(Flight.route(Route))).define(Route.average_departure_delay(delay)) +m.where(delay := Delay.ref(), Route, delay == avg(Flight.arrival_delay).per(Route).where(Flight.route(Route))).define(Route.average_arrival_delay(delay)) diff --git a/converters/ontology/tests/relationalai/snapshots/test_converter/test_converted_model_decompiles_to_expected_pyrel/retail/retail.py b/converters/ontology/tests/relationalai/snapshots/test_converter/test_converted_model_decompiles_to_expected_pyrel/retail/retail.py new file mode 100644 index 00000000..32284c90 --- /dev/null +++ b/converters/ontology/tests/relationalai/snapshots/test_converter/test_converted_model_decompiles_to_expected_pyrel/retail/retail.py @@ -0,0 +1,179 @@ +# 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 relationalai.semantics import Date, Decimal, Integer, Model, String, avg, sum + +m = Model("model") + +Amount = m.Concept("Amount", extends=[Decimal]) +EmployeeNr = m.Concept("EmployeeNr", extends=[Integer]) +RegisterId = m.Concept("RegisterId", extends=[Integer]) +StoreNr = m.Concept("StoreNr", extends=[Integer]) +TransactionNr = m.Concept("TransactionNr", extends=[Integer]) +UniversalProductCode = m.Concept("UniversalProductCode", extends=[Integer]) +ClassNr = m.Concept("ClassNr", extends=[Integer]) +CategoryName = m.Concept("CategoryName", extends=[String]) +ClassLabelName = m.Concept("ClassLabelName", extends=[String]) +RegionName = m.Concept("RegionName", extends=[String]) +OrderNr = m.Concept("OrderNr", extends=[Integer]) +PaymentId = m.Concept("PaymentId", extends=[Integer]) +Employee = m.Concept("Employee") +Contractor = m.Concept("Contractor", extends=[Employee]) +PermanentEmployee = m.Concept("PermanentEmployee", extends=[Employee]) +Item = m.Concept("Item") +Category = m.Concept("Category") +ClassLabel = m.Concept("ClassLabel") +Class = m.Concept("Class") +Region = m.Concept("Region") +Store = m.Concept("Store") +Terminal = m.Concept("Terminal") +Transaction = m.Concept("Transaction") +Order = m.Concept("Order") +OrderLineItem = m.Concept("OrderLineItem") +Payment = m.Concept("Payment") +CardPayment = m.Concept("CardPayment", extends=[Payment]) +CashPayment = m.Concept("CashPayment", extends=[Payment]) +db_schema_employees = m.Table("DB.SCHEMA.EMPLOYEES", schema={'EMPNR': Integer, 'LASTNAME': String, 'FIRSTNAME': String, 'MGR': Integer, 'ADDR': String, 'CITY': String, 'STATE': String, 'ZIP': String}) +db_schema_contractors = m.Table("DB.SCHEMA.CONTRACTORS", schema={'EMPNR': Integer, 'HOURLYRATE': Integer}) +db_schema_permanent_employees = m.Table("DB.SCHEMA.PERMANENT_EMPLOYEES", schema={'EMPNR': Integer, 'SALARY': Integer}) +db_schema_locations = m.Table("DB.SCHEMA.LOCATIONS", schema={'STORENR': Integer, 'MGR': Integer, 'REGION': String, 'ADDR': String, 'CITY': String, 'STATE': String, 'ZIP': String}) +db_schema_products = m.Table("DB.SCHEMA.PRODUCTS", schema={'UPC': Integer, 'DESCRIPTION': String, 'CLASSNR': Integer, 'CLASSLABEL': String, 'CATEGORY': String}) +db_schema_transactions = m.Table("DB.SCHEMA.TRANSACTIONS", schema={'TXN': Integer, 'SKU': Integer, 'TERMINAL': Integer, 'AMT': String, 'REFUNDYN': String, 'TIMESTAMP': String}) +db_schema_terminals = m.Table("DB.SCHEMA.TERMINALS", schema={'ID': Integer, 'STORE': Integer, 'SVCDATE': String}) +db_schema_lineitems = m.Table("DB.SCHEMA.LINEITEMS", schema={'QUANTITY': String, 'EXTENDEDPRICE': Decimal, 'DISCOUNT': Decimal, 'TAX': String, 'ORDERNR': Integer, 'LINEITEMNR': Integer, 'UPC': String, 'STATUS': String, 'SHIPMODE': String, 'SHIPDATE': String, 'RECEIPTDATE': String}) +db_schema_orders = m.Table("DB.SCHEMA.ORDERS", schema={'NR': Integer, 'STORE': String, 'ORDDATE': Date}) +db_schema_payments = m.Table("DB.SCHEMA.PAYMENTS", schema={'ID': Integer, 'ORDERNR': String, 'PAYMENTDATE': String, 'AMOUNT': Decimal, 'TYPE': String, 'CARDNUMBER': String, 'CARDEXPIRY': String, 'CARDHOLDERNAME': String, 'CASHRECEIVED': Decimal}) +db_schema_retail_facts = m.Table("DB.SCHEMA.RETAIL_FACTS", schema={'ITEM_NR': Integer, 'STORE_NR': Integer, 'WEEK_NR': Integer, 'SLS': Decimal, 'RETURNS': Decimal}) + +Employee.nr = m.Property(f"{Employee} is identified by {EmployeeNr}") +Item.upc = m.Property(f"{Item} is identified by {UniversalProductCode}") +Category.name = m.Property(f"{Category} is identified by {CategoryName}") +ClassLabel.name = m.Property(f"{ClassLabel} is identified by {ClassLabelName}") +Class.nr = m.Property(f"{Class} is identified by {ClassNr}") +Region.name = m.Property(f"{Region} is identified by {RegionName}") +Store.nr = m.Property(f"{Store} is identified by {StoreNr}") +Terminal.id = m.Property(f"{Terminal} is identified by {RegisterId}") +Transaction.nr = m.Property(f"{Transaction} is identified by {TransactionNr}") +Order.nr = m.Property(f"{Order} is identified by {OrderNr}") +OrderLineItem.order = m.Property(f"{OrderLineItem} belongs to {Order}") +OrderLineItem.nr = m.Property(f"{OrderLineItem} has line item number {Integer}") +Payment.id = m.Property(f"{Payment} is identified by {PaymentId}") +Employee.reportsTo = m.Property(f"{Employee} reports to {Employee:mgr}") +Employee.firstName = m.Property(f"{Employee} {String}") +Employee.lastName = m.Property(f"{Employee} {String}") +Contractor.hourlyRate = m.Property(f"{Contractor} is paid hourly {Integer}") +PermanentEmployee.salary = m.Property(f"{PermanentEmployee} has monthly salary {Integer}") +Item.description = m.Property(f"{Item} {String}") +Item.itemToClass = m.Property(f"{Item} is grouped by {Class}") +Item.weekly_sales = m.Property(f"{Item} sold at {Store} in week {Integer} for {Decimal}") +Item.weekly_gross_sales = m.Property(f"{Item} sold at {Store} in week {Integer} for gross {Decimal}") +Item.weekly_gross_returns = m.Property(f"{Item} returned at {Store} in week {Integer} for {Decimal}") +Item.weekly_net_revenue = m.Property(f"{Item} sold at {Store} in week {Integer} for net {Decimal}") +Item.item_store_sales = m.Property(f"{Item} sold at {Store} for total {Decimal}") +Class.classToCategory = m.Property(f"{Class} is part of {Category}") +Class.classToLabel = m.Property(f"{Class} has {ClassLabel}") +Store.storeToManager = m.Property(f"{Store} is managed by {Employee}") +Store.storeToRegion = m.Property(f"{Store} belongs to {Region}") +Store.avgReturns = m.Property(f"{Store} reports returns of {Item} for average {Amount:amt}") +Store.avgReturnsWithMarkup = m.Property(f"{Store} reports returns of {Item} for 10%-marked-up average {Amount:amt}") +Terminal.deployedTo = m.Property(f"{Terminal} is deployed to {Store}") +Transaction.registeredBy = m.Property(f"{Transaction} is registered by {Terminal}") +Transaction.returns = m.Property(f"{Transaction} refunds {Item} for {Amount}") +Transaction.sales = m.Property(f"{Transaction} sells {Item} for {Amount}") +Transaction.transactionTerminalStore = m.Relationship(f"{Transaction} registered at {Terminal} deployed to {Store}") +Transaction.storeNrViaBinaryChain = m.Property(f"{Transaction} was registered at a terminal deployed to store {StoreNr:store_nr}") +Transaction.storeNrViaTernaryChain = m.Property(f"{Transaction} occurred at store {StoreNr:store_nr}") +Transaction.transactionRegion = m.Property(f"{Transaction} occurred in {Region}") +Transaction.transactionStoreManager = m.Property(f"{Transaction} associated with store manager {Employee}") +Order.orderDate = m.Property(f"{Order} has {Date}") +OrderLineItem.lineitem_extended_price = m.Property(f"{OrderLineItem} has extended price {Decimal:lineitem_extended_price}") +OrderLineItem.lineitem_discount = m.Property(f"{OrderLineItem} has discount factor {Decimal:lineitem_discount}") +OrderLineItem.net_revenue = m.Property(f"{OrderLineItem} has net revenue {Decimal:net_revenue}") +OrderLineItem.discounted_amount = m.Property(f"{OrderLineItem} has discounted amount {Amount:discounted_amount}") +Payment.type = m.Property(f"{Payment} {String}") +Payment.amount = m.Property(f"{Payment} {Decimal}") +CardPayment.cardNumber = m.Property(f"{CardPayment} {String}") +CardPayment.cardHolderName = m.Property(f"{CardPayment} {String}") +CashPayment.cashReceived = m.Property(f"{CashPayment} {Decimal}") +CashPayment.change = m.Property(f"{CashPayment} {Decimal}") + +Employee.identify_by(Employee.nr) +Item.identify_by(Item.upc) +Category.identify_by(Category.name) +ClassLabel.identify_by(ClassLabel.name) +Class.identify_by(Class.nr) +Region.identify_by(Region.name) +Store.identify_by(Store.nr) +Terminal.identify_by(Terminal.id) +Transaction.identify_by(Transaction.nr) +Order.identify_by(Order.nr) +OrderLineItem.identify_by(OrderLineItem.order) +OrderLineItem.identify_by(OrderLineItem.nr) +Payment.identify_by(Payment.id) +m.define(Employee.new(nr=db_schema_employees.EMPNR)) +m.where(employee := Employee.to_identity(nr=db_schema_employees.EMPNR, unsafe=True), employee_2 := Employee.lookup(nr=db_schema_employees.MGR)).define(employee.reportsTo(employee_2)) +m.where(employee := Employee.to_identity(nr=db_schema_employees.EMPNR, unsafe=True)).define(employee.firstName(db_schema_employees.FIRSTNAME)) +m.where(employee := Employee.to_identity(nr=db_schema_employees.EMPNR, unsafe=True)).define(employee.lastName(db_schema_employees.LASTNAME)) +m.where(contractor := Contractor.lookup(nr=db_schema_contractors.EMPNR)).define(contractor.hourlyRate(db_schema_contractors.HOURLYRATE)) +m.where(permanentemployee := PermanentEmployee.lookup(nr=db_schema_permanent_employees.EMPNR)).define(permanentemployee.salary(db_schema_permanent_employees.SALARY)) +m.define(Item.new(upc=db_schema_products.UPC)) +m.where(item := Item.to_identity(upc=db_schema_products.UPC, unsafe=True), class_ := Class.lookup(nr=db_schema_products.CLASSNR)).define(item.itemToClass(class_)) +m.where(item := Item.to_identity(upc=db_schema_products.UPC, unsafe=True)).define(item.description(db_schema_products.DESCRIPTION)) +m.define(Item.new(upc=db_schema_retail_facts.ITEM_NR)) +m.where(item := Item.to_identity(upc=db_schema_retail_facts.ITEM_NR, unsafe=True), store := Store.lookup(nr=db_schema_retail_facts.STORE_NR)).define(item.weekly_sales(store, db_schema_retail_facts.WEEK_NR, db_schema_retail_facts.SLS)) +m.where(item := Item.to_identity(upc=db_schema_retail_facts.ITEM_NR, unsafe=True), store := Store.lookup(nr=db_schema_retail_facts.STORE_NR)).define(item.weekly_gross_sales(store, db_schema_retail_facts.WEEK_NR, (db_schema_retail_facts.SLS + db_schema_retail_facts.RETURNS))) +m.where(item := Item.to_identity(upc=db_schema_retail_facts.ITEM_NR, unsafe=True), store := Store.lookup(nr=db_schema_retail_facts.STORE_NR)).define(item.weekly_gross_returns(store, db_schema_retail_facts.WEEK_NR, db_schema_retail_facts.RETURNS)) +m.where(item := Item.to_identity(upc=db_schema_retail_facts.ITEM_NR, unsafe=True), store := Store.lookup(nr=db_schema_retail_facts.STORE_NR)).define(item.weekly_net_revenue(store, db_schema_retail_facts.WEEK_NR, (((db_schema_retail_facts.SLS * 0.9) - (db_schema_retail_facts.RETURNS * 1.1)) + 100))) +m.where(item := Item.to_identity(upc=db_schema_retail_facts.ITEM_NR, unsafe=True), store := Store.lookup(nr=db_schema_retail_facts.STORE_NR), result := sum(db_schema_retail_facts.SLS).per(db_schema_retail_facts.ITEM_NR, db_schema_retail_facts.STORE_NR).where(db_schema_retail_facts.SLS > 0)).define(item.item_store_sales(store, result)) +m.define(Category.new(name=db_schema_products.CATEGORY)) +m.define(ClassLabel.new(name=db_schema_products.CLASSLABEL)) +m.define(Class.new(nr=db_schema_products.CLASSNR)) +m.where(class_ := Class.to_identity(nr=db_schema_products.CLASSNR, unsafe=True), category := Category.lookup(name=db_schema_products.CATEGORY)).define(class_.classToCategory(category)) +m.where(class_ := Class.to_identity(nr=db_schema_products.CLASSNR, unsafe=True), classlabel := ClassLabel.lookup(name=db_schema_products.CLASSLABEL)).define(class_.classToLabel(classlabel)) +m.define(Region.new(name=db_schema_locations.REGION)) +m.define(Store.new(nr=db_schema_locations.STORENR)) +m.where(store := Store.to_identity(nr=db_schema_locations.STORENR, unsafe=True), employee := Employee.lookup(nr=db_schema_locations.MGR)).define(store.storeToManager(employee)) +m.where(store := Store.to_identity(nr=db_schema_locations.STORENR, unsafe=True), region := Region.lookup(name=db_schema_locations.REGION)).define(store.storeToRegion(region)) +m.define(Terminal.new(id=db_schema_terminals.ID)) +m.where(terminal := Terminal.to_identity(id=db_schema_terminals.ID, unsafe=True), store := Store.lookup(nr=db_schema_terminals.STORE)).define(terminal.deployedTo(store)) +m.define(Transaction.new(nr=db_schema_transactions.TXN)) +m.where(transaction := Transaction.to_identity(nr=db_schema_transactions.TXN, unsafe=True), terminal := Terminal.lookup(id=db_schema_transactions.TERMINAL)).define(transaction.registeredBy(terminal)) +m.where(transaction := Transaction.to_identity(nr=db_schema_transactions.TXN, unsafe=True), item := Item.lookup(upc=db_schema_transactions.SKU), db_schema_transactions.REFUNDYN == "Y").define(transaction.returns(item, Amount(db_schema_transactions.AMT))) +m.where(transaction := Transaction.to_identity(nr=db_schema_transactions.TXN, unsafe=True), item := Item.lookup(upc=db_schema_transactions.SKU), m.not_(db_schema_transactions.REFUNDYN == "Y")).define(transaction.sales(item, Amount(db_schema_transactions.AMT))) +m.define(Order.new(nr=db_schema_orders.NR)) +m.where(order := Order.to_identity(nr=db_schema_orders.NR, unsafe=True)).define(order.orderDate(db_schema_orders.ORDDATE)) +m.where(order := Order.lookup(nr=db_schema_lineitems.ORDERNR)).define(OrderLineItem.new(order=order, nr=db_schema_lineitems.LINEITEMNR)) +m.where(orderlineitem := OrderLineItem.to_identity(order=order, nr=db_schema_lineitems.LINEITEMNR, unsafe=True), order := Order.lookup(nr=db_schema_lineitems.ORDERNR)).define(orderlineitem.lineitem_extended_price(db_schema_lineitems.EXTENDEDPRICE)) +m.where(orderlineitem := OrderLineItem.to_identity(order=order, nr=db_schema_lineitems.LINEITEMNR, unsafe=True), order := Order.lookup(nr=db_schema_lineitems.ORDERNR)).define(orderlineitem.lineitem_discount(db_schema_lineitems.DISCOUNT)) +m.define(Payment.new(id=db_schema_payments.ID)) +m.where(payment := Payment.to_identity(id=db_schema_payments.ID, unsafe=True)).define(payment.type(db_schema_payments.TYPE)) +m.where(payment := Payment.to_identity(id=db_schema_payments.ID, unsafe=True)).define(payment.amount(db_schema_payments.AMOUNT)) +m.where(cardpayment := CardPayment.lookup(id=db_schema_payments.ID), db_schema_payments.TYPE == "card").define(cardpayment.cardNumber(db_schema_payments.CARDNUMBER)) +m.where(cardpayment := CardPayment.lookup(id=db_schema_payments.ID), db_schema_payments.TYPE == "card").define(cardpayment.cardHolderName(db_schema_payments.CARDHOLDERNAME)) +m.where(cashpayment := CashPayment.lookup(id=db_schema_payments.ID), db_schema_payments.TYPE == "cash").define(cashpayment.cashReceived(db_schema_payments.CASHRECEIVED)) +m.where(cashpayment := CashPayment.lookup(id=db_schema_payments.ID), db_schema_payments.TYPE == "cash").define(cashpayment.change((db_schema_payments.CASHRECEIVED - db_schema_payments.AMOUNT))) +m.where(Payment, Payment.type == "cash").define(CashPayment(Payment)) +m.where(Payment, Payment.type == "card").define(CardPayment(Payment)) +m.where(amt := Amount.ref(), Item, Store, amt == avg(Amount).per(Item, Store).where(Transaction, Terminal, Transaction.registeredBy(Terminal), Transaction.returns(Item, Amount), Terminal.deployedTo(Store))).define(Store.avgReturns(Item, amt)) +m.where(amt_2 := Amount.ref(), Item, Store, amt_2 == Amount((avg(Amount).per(Item, Store).where(Transaction, Terminal, Transaction.registeredBy(Terminal), Transaction.returns(Item, Amount), Terminal.deployedTo(Store)) * 1.1))).define(Store.avgReturnsWithMarkup(Item, amt_2)) +m.where(Transaction, Terminal, Transaction.registeredBy(Terminal), Store, Terminal.deployedTo(Store)).define(Transaction.transactionTerminalStore(Terminal, Store)) +m.where(store_nr := StoreNr.ref(), Transaction, Terminal, Transaction.registeredBy(Terminal), store_nr == Terminal.deployedTo.nr).define(Transaction.storeNrViaBinaryChain(store_nr)) +m.where(store_nr_2 := StoreNr.ref(), Transaction, Terminal, Transaction.transactionTerminalStore(Terminal, Store), store_nr_2 == Store.nr).define(Transaction.storeNrViaTernaryChain(store_nr_2)) +m.where(Transaction, Terminal, Transaction.registeredBy(Terminal), Store, Terminal.deployedTo(Store), Region, Store.storeToRegion(Region)).define(Transaction.transactionRegion(Region)) +m.where(Transaction, Terminal, Transaction.registeredBy(Terminal), Store, Terminal.deployedTo(Store), Employee, Store.storeToManager(Employee)).define(Transaction.transactionStoreManager(Employee)) +m.where(extended_price := Decimal.ref(), discount := Decimal.ref(), net_revenue := Decimal.ref(), OrderLineItem, extended_price == OrderLineItem.lineitem_extended_price, discount == OrderLineItem.lineitem_discount, net_revenue == (extended_price * (1 - discount))).define(OrderLineItem.net_revenue(net_revenue)) +m.where(OrderLineItem, OrderLineItem.lineitem_extended_price, OrderLineItem.lineitem_discount, Amount == Amount((OrderLineItem.lineitem_extended_price - OrderLineItem.lineitem_discount))).define(OrderLineItem.discounted_amount(Amount)) diff --git a/converters/ontology/tests/relationalai/snapshots/test_converter/test_converted_model_decompiles_to_expected_pyrel/tpch/tpch.py b/converters/ontology/tests/relationalai/snapshots/test_converter/test_converted_model_decompiles_to_expected_pyrel/tpch/tpch.py new file mode 100644 index 00000000..980e0957 --- /dev/null +++ b/converters/ontology/tests/relationalai/snapshots/test_converter/test_converted_model_decompiles_to_expected_pyrel/tpch/tpch.py @@ -0,0 +1,192 @@ +# 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 relationalai.semantics import Date, Decimal, Integer, Model, String + +m = Model("model") + +Region = m.Concept("Region") +Nation = m.Concept("Nation") +Supplier = m.Concept("Supplier") +Part = m.Concept("Part") +PartSupplier = m.Concept("PartSupplier") +CustOrder = m.Concept("CustOrder") +OrderLineitem = m.Concept("OrderLineitem") +Customer = m.Concept("Customer") +kse_tpch_sf1_customer = m.Table("KSE.TPCH_SF1.CUSTOMER", schema={'C_CUSTKEY': Integer, 'C_NAME': String, 'C_ADDRESS': String, 'C_NATIONKEY': Integer, 'C_PHONE': String, 'C_ACCTBAL': Decimal, 'C_MKTSEGMENT': String, 'C_COMMENT': String}) +kse_tpch_sf1_orders = m.Table("KSE.TPCH_SF1.ORDERS", schema={'O_ORDERKEY': Integer, 'O_CUSTKEY': Integer, 'O_ORDERSTATUS': String, 'O_TOTALPRICE': Decimal, 'O_ORDERDATE': Date, 'O_ORDERPRIORITY': String, 'O_CLERK': String, 'O_SHIPPRIORITY': Decimal, 'O_COMMENT': String}) +kse_tpch_sf1_lineitem = m.Table("KSE.TPCH_SF1.LINEITEM", schema={'L_ORDERKEY': Integer, 'L_PARTKEY': Integer, 'L_SUPPKEY': Integer, 'L_LINENUMBER': Integer, 'L_QUANTITY': Decimal, 'L_EXTENDEDPRICE': Decimal, 'L_DISCOUNT': Decimal, 'L_TAX': Decimal, 'L_RETURNFLAG': String, 'L_LINESTATUS': String, 'L_SHIPDATE': Date, 'L_COMMITDATE': Date, 'L_RECEIPTDATE': Date, 'L_SHIPINSTRUCT': String, 'L_SHIPMODE': String, 'L_COMMENT': String}) +kse_tpch_sf1_partsupp = m.Table("KSE.TPCH_SF1.PARTSUPP", schema={'PS_PARTKEY': Integer, 'PS_SUPPKEY': Integer, 'PS_AVAILQTY': Decimal, 'PS_SUPPLYCOST': Decimal, 'PS_COMMENT': String}) +kse_tpch_sf1_part = m.Table("KSE.TPCH_SF1.PART", schema={'P_PARTKEY': Integer, 'P_NAME': String, 'P_MFGR': String, 'P_BRAND': String, 'P_TYPE': String, 'P_SIZE': Integer, 'P_CONTAINER': String, 'P_RETAILPRICE': Decimal, 'P_COMMENT': String}) +kse_tpch_sf1_supplier = m.Table("KSE.TPCH_SF1.SUPPLIER", schema={'S_SUPPKEY': Integer, 'S_NAME': String, 'S_ADDRESS': String, 'S_NATIONKEY': Integer, 'S_PHONE': String, 'S_ACCTBAL': Decimal, 'S_COMMENT': String}) +kse_tpch_sf1_nation = m.Table("KSE.TPCH_SF1.NATION", schema={'N_NATIONKEY': Integer, 'N_NAME': String, 'N_REGIONKEY': Integer, 'N_COMMENT': String}) +kse_tpch_sf1_region = m.Table("KSE.TPCH_SF1.REGION", schema={'R_REGIONKEY': Integer, 'R_NAME': String, 'R_COMMENT': String}) + +Region.region_key = m.Property(f"{Region} is identified by {Integer}") +Nation.nation_key = m.Property(f"{Nation} is identified by {Integer}") +Supplier.supplier_key = m.Property(f"{Supplier} is identified by {Integer}") +Part.part_key = m.Property(f"{Part} is identified by {Integer}") +PartSupplier.part_key = m.Property(f"{PartSupplier} is identified by part {Integer}") +PartSupplier.supplier_key = m.Property(f"{PartSupplier} is identified by supplier {Integer}") +CustOrder.order_key = m.Property(f"{CustOrder} is identified by {Integer}") +OrderLineitem.order_key = m.Property(f"{OrderLineitem} is identified by order {Integer}") +OrderLineitem.order_lineitem_number = m.Property(f"{OrderLineitem} is identified by line {Integer}") +Customer.customer_key = m.Property(f"{Customer} is identified by {Integer}") +Region.region_comment = m.Property(f"{Region} has {String:region_comment}") +Region.region_name = m.Property(f"{Region} has {String:region_name}") +Nation.nation_comment = m.Property(f"{Nation} has {String:nation_comment}") +Nation.region_key = m.Property(f"{Nation} has {Integer:region_key}") +Nation.nation_name = m.Property(f"{Nation} has {String:nation_name}") +Nation.North_America = m.Relationship(f"{Nation} has North America filter") +Nation.nations_to_regions = m.Property(f"{Nation} maps to {Region}") +Supplier.supplier_address = m.Property(f"{Supplier} has {String:supplier_address}") +Supplier.supplier_phone = m.Property(f"{Supplier} has {String:supplier_phone}") +Supplier.supplier_account_balance = m.Property(f"{Supplier} has {Decimal:supplier_account_balance}") +Supplier.supplier_comment = m.Property(f"{Supplier} has {String:supplier_comment}") +Supplier.supplier_name = m.Property(f"{Supplier} has {String:supplier_name}") +Supplier.nation_key = m.Property(f"{Supplier} has {Integer:nation_key}") +Supplier.suppliers_to_nations = m.Property(f"{Supplier} maps to {Nation}") +Part.part_retail_price = m.Property(f"{Part} has {Decimal:part_retail_price}") +Part.part_comment = m.Property(f"{Part} has {String:part_comment}") +Part.part_name = m.Property(f"{Part} has {String:part_name}") +Part.part_manufacturer_name = m.Property(f"{Part} has {String:part_manufacturer_name}") +Part.part_brand = m.Property(f"{Part} has {String:part_brand}") +Part.part_type = m.Property(f"{Part} has {String:part_type}") +Part.part_container = m.Property(f"{Part} has {String:part_container}") +Part.part_size = m.Property(f"{Part} has {Integer:part_size}") +PartSupplier.available_quantity = m.Property(f"{PartSupplier} has {Decimal:available_quantity}") +PartSupplier.part_supplier_supply_cost = m.Property(f"{PartSupplier} has {Decimal:part_supplier_supply_cost}") +PartSupplier.part_supplier_comment = m.Property(f"{PartSupplier} has {String:part_supplier_comment}") +PartSupplier.part_suppliers_to_part = m.Property(f"{PartSupplier} maps to {Part}") +PartSupplier.part_suppliers_to_supplier = m.Property(f"{PartSupplier} maps to {Supplier}") +CustOrder.order_gross_total = m.Property(f"{CustOrder} has {Decimal:order_gross_total}") +CustOrder.order_clerk = m.Property(f"{CustOrder} has {String:order_clerk}") +CustOrder.order_ship_priority = m.Property(f"{CustOrder} has {Decimal:order_ship_priority}") +CustOrder.order_comment = m.Property(f"{CustOrder} has {String:order_comment}") +CustOrder.customer_key = m.Property(f"{CustOrder} has {Integer:customer_key}") +CustOrder.order_status = m.Property(f"{CustOrder} has {String:order_status}") +CustOrder.order_priority = m.Property(f"{CustOrder} has {String:order_priority}") +CustOrder.order_date = m.Property(f"{CustOrder} has {Date:order_date}") +CustOrder.Yesterday = m.Relationship(f"{CustOrder} has Yesterday filter") +CustOrder.YTD = m.Relationship(f"{CustOrder} has YTD filter") +CustOrder.orders_to_customers = m.Property(f"{CustOrder} maps to {Customer}") +OrderLineitem.lineitem_quantity = m.Property(f"{OrderLineitem} has {Decimal:lineitem_quantity}") +OrderLineitem.lineitem_extended_price = m.Property(f"{OrderLineitem} has {Decimal:lineitem_extended_price}") +OrderLineitem.lineitem_discount = m.Property(f"{OrderLineitem} has {Decimal:lineitem_discount}") +OrderLineitem.lineitem_tax = m.Property(f"{OrderLineitem} has {Decimal:lineitem_tax}") +OrderLineitem.lineitem_comment = m.Property(f"{OrderLineitem} has {String:lineitem_comment}") +OrderLineitem.net_revenue = m.Property(f"{OrderLineitem} has {Decimal:net_revenue}") +OrderLineitem.gross_revenue = m.Property(f"{OrderLineitem} has {Decimal:gross_revenue}") +OrderLineitem.part_key = m.Property(f"{OrderLineitem} has {Integer:part_key}") +OrderLineitem.supplier_key = m.Property(f"{OrderLineitem} has {Integer:supplier_key}") +OrderLineitem.lineitem_return_flag = m.Property(f"{OrderLineitem} has {String:lineitem_return_flag}") +OrderLineitem.lineitem_status = m.Property(f"{OrderLineitem} has {String:lineitem_status}") +OrderLineitem.lineitem_shipping_instructions = m.Property(f"{OrderLineitem} has {String:lineitem_shipping_instructions}") +OrderLineitem.lineitem_shipping_mode = m.Property(f"{OrderLineitem} has {String:lineitem_shipping_mode}") +OrderLineitem.lineitem_ship_date = m.Property(f"{OrderLineitem} has {Date:lineitem_ship_date}") +OrderLineitem.lineitem_commit_date = m.Property(f"{OrderLineitem} has {Date:lineitem_commit_date}") +OrderLineitem.lineitem_receipt_date = m.Property(f"{OrderLineitem} has {Date:lineitem_receipt_date}") +OrderLineitem.lineitems_to_orders = m.Property(f"{OrderLineitem} maps to {CustOrder}") +OrderLineitem.order_lineitems_to_part_suppliers = m.Property(f"{OrderLineitem} maps to {PartSupplier}") +Customer.customer_address = m.Property(f"{Customer} has {String:customer_address}") +Customer.customer_phone = m.Property(f"{Customer} has {String:customer_phone}") +Customer.customer_account_balance = m.Property(f"{Customer} has {Decimal:customer_account_balance}") +Customer.customer_comment = m.Property(f"{Customer} has {String:customer_comment}") +Customer.nation_key = m.Property(f"{Customer} has {Integer:nation_key}") +Customer.customer_name = m.Property(f"{Customer} has {String:customer_name}") +Customer.customer_market_segment = m.Property(f"{Customer} has {String:customer_market_segment}") +Customer.automobile_market_segment = m.Relationship(f"{Customer} has automobile_market_segment filter") +Customer.top_10_customers_by_higest_account_balance = m.Relationship(f"{Customer} has top_10_customers_by_higest_account_balance filter") +Customer.customers_to_nations = m.Property(f"{Customer} maps to {Nation}") + +Region.identify_by(Region.region_key) +Nation.identify_by(Nation.nation_key) +Supplier.identify_by(Supplier.supplier_key) +Part.identify_by(Part.part_key) +PartSupplier.identify_by(PartSupplier.part_key) +PartSupplier.identify_by(PartSupplier.supplier_key) +CustOrder.identify_by(CustOrder.order_key) +OrderLineitem.identify_by(OrderLineitem.order_key) +OrderLineitem.identify_by(OrderLineitem.order_lineitem_number) +Customer.identify_by(Customer.customer_key) +m.define(Region.new(region_key=kse_tpch_sf1_region.R_REGIONKEY)) +m.where(region := Region.to_identity(region_key=kse_tpch_sf1_region.R_REGIONKEY, unsafe=True)).define(region.region_name(kse_tpch_sf1_region.R_NAME)) +m.where(region := Region.to_identity(region_key=kse_tpch_sf1_region.R_REGIONKEY, unsafe=True)).define(region.region_comment(kse_tpch_sf1_region.R_COMMENT)) +m.define(Nation.new(nation_key=kse_tpch_sf1_nation.N_NATIONKEY)) +m.where(nation := Nation.to_identity(nation_key=kse_tpch_sf1_nation.N_NATIONKEY, unsafe=True)).define(nation.nation_name(kse_tpch_sf1_nation.N_NAME)) +m.where(nation := Nation.to_identity(nation_key=kse_tpch_sf1_nation.N_NATIONKEY, unsafe=True)).define(nation.nation_comment(kse_tpch_sf1_nation.N_COMMENT)) +m.where(nation := Nation.to_identity(nation_key=kse_tpch_sf1_nation.N_NATIONKEY, unsafe=True)).define(nation.region_key(kse_tpch_sf1_nation.N_REGIONKEY)) +m.define(Supplier.new(supplier_key=kse_tpch_sf1_supplier.S_SUPPKEY)) +m.where(supplier := Supplier.to_identity(supplier_key=kse_tpch_sf1_supplier.S_SUPPKEY, unsafe=True)).define(supplier.supplier_name(kse_tpch_sf1_supplier.S_NAME)) +m.where(supplier := Supplier.to_identity(supplier_key=kse_tpch_sf1_supplier.S_SUPPKEY, unsafe=True)).define(supplier.supplier_address(kse_tpch_sf1_supplier.S_ADDRESS)) +m.where(supplier := Supplier.to_identity(supplier_key=kse_tpch_sf1_supplier.S_SUPPKEY, unsafe=True)).define(supplier.supplier_phone(kse_tpch_sf1_supplier.S_PHONE)) +m.where(supplier := Supplier.to_identity(supplier_key=kse_tpch_sf1_supplier.S_SUPPKEY, unsafe=True)).define(supplier.supplier_account_balance(kse_tpch_sf1_supplier.S_ACCTBAL)) +m.where(supplier := Supplier.to_identity(supplier_key=kse_tpch_sf1_supplier.S_SUPPKEY, unsafe=True)).define(supplier.supplier_comment(kse_tpch_sf1_supplier.S_COMMENT)) +m.where(supplier := Supplier.to_identity(supplier_key=kse_tpch_sf1_supplier.S_SUPPKEY, unsafe=True)).define(supplier.nation_key(kse_tpch_sf1_supplier.S_NATIONKEY)) +m.define(Part.new(part_key=kse_tpch_sf1_part.P_PARTKEY)) +m.where(part := Part.to_identity(part_key=kse_tpch_sf1_part.P_PARTKEY, unsafe=True)).define(part.part_name(kse_tpch_sf1_part.P_NAME)) +m.where(part := Part.to_identity(part_key=kse_tpch_sf1_part.P_PARTKEY, unsafe=True)).define(part.part_manufacturer_name(kse_tpch_sf1_part.P_MFGR)) +m.where(part := Part.to_identity(part_key=kse_tpch_sf1_part.P_PARTKEY, unsafe=True)).define(part.part_brand(kse_tpch_sf1_part.P_BRAND)) +m.where(part := Part.to_identity(part_key=kse_tpch_sf1_part.P_PARTKEY, unsafe=True)).define(part.part_type(kse_tpch_sf1_part.P_TYPE)) +m.where(part := Part.to_identity(part_key=kse_tpch_sf1_part.P_PARTKEY, unsafe=True)).define(part.part_size(kse_tpch_sf1_part.P_SIZE)) +m.where(part := Part.to_identity(part_key=kse_tpch_sf1_part.P_PARTKEY, unsafe=True)).define(part.part_container(kse_tpch_sf1_part.P_CONTAINER)) +m.where(part := Part.to_identity(part_key=kse_tpch_sf1_part.P_PARTKEY, unsafe=True)).define(part.part_retail_price(kse_tpch_sf1_part.P_RETAILPRICE)) +m.where(part := Part.to_identity(part_key=kse_tpch_sf1_part.P_PARTKEY, unsafe=True)).define(part.part_comment(kse_tpch_sf1_part.P_COMMENT)) +m.define(PartSupplier.new(part_key=kse_tpch_sf1_partsupp.PS_PARTKEY, supplier_key=kse_tpch_sf1_partsupp.PS_SUPPKEY)) +m.where(partsupplier := PartSupplier.to_identity(part_key=kse_tpch_sf1_partsupp.PS_PARTKEY, supplier_key=kse_tpch_sf1_partsupp.PS_SUPPKEY, unsafe=True)).define(partsupplier.available_quantity(kse_tpch_sf1_partsupp.PS_AVAILQTY)) +m.where(partsupplier := PartSupplier.to_identity(part_key=kse_tpch_sf1_partsupp.PS_PARTKEY, supplier_key=kse_tpch_sf1_partsupp.PS_SUPPKEY, unsafe=True)).define(partsupplier.part_supplier_supply_cost(kse_tpch_sf1_partsupp.PS_SUPPLYCOST)) +m.where(partsupplier := PartSupplier.to_identity(part_key=kse_tpch_sf1_partsupp.PS_PARTKEY, supplier_key=kse_tpch_sf1_partsupp.PS_SUPPKEY, unsafe=True)).define(partsupplier.part_supplier_comment(kse_tpch_sf1_partsupp.PS_COMMENT)) +m.define(CustOrder.new(order_key=kse_tpch_sf1_orders.O_ORDERKEY)) +m.where(custorder := CustOrder.to_identity(order_key=kse_tpch_sf1_orders.O_ORDERKEY, unsafe=True)).define(custorder.order_gross_total(kse_tpch_sf1_orders.O_TOTALPRICE)) +m.where(custorder := CustOrder.to_identity(order_key=kse_tpch_sf1_orders.O_ORDERKEY, unsafe=True)).define(custorder.order_clerk(kse_tpch_sf1_orders.O_CLERK)) +m.where(custorder := CustOrder.to_identity(order_key=kse_tpch_sf1_orders.O_ORDERKEY, unsafe=True)).define(custorder.order_ship_priority(kse_tpch_sf1_orders.O_SHIPPRIORITY)) +m.where(custorder := CustOrder.to_identity(order_key=kse_tpch_sf1_orders.O_ORDERKEY, unsafe=True)).define(custorder.order_comment(kse_tpch_sf1_orders.O_COMMENT)) +m.where(custorder := CustOrder.to_identity(order_key=kse_tpch_sf1_orders.O_ORDERKEY, unsafe=True)).define(custorder.customer_key(kse_tpch_sf1_orders.O_CUSTKEY)) +m.where(custorder := CustOrder.to_identity(order_key=kse_tpch_sf1_orders.O_ORDERKEY, unsafe=True)).define(custorder.order_status(kse_tpch_sf1_orders.O_ORDERSTATUS)) +m.where(custorder := CustOrder.to_identity(order_key=kse_tpch_sf1_orders.O_ORDERKEY, unsafe=True)).define(custorder.order_priority(kse_tpch_sf1_orders.O_ORDERPRIORITY)) +m.where(custorder := CustOrder.to_identity(order_key=kse_tpch_sf1_orders.O_ORDERKEY, unsafe=True)).define(custorder.order_date(kse_tpch_sf1_orders.O_ORDERDATE)) +m.define(OrderLineitem.new(order_key=kse_tpch_sf1_lineitem.L_ORDERKEY, order_lineitem_number=kse_tpch_sf1_lineitem.L_LINENUMBER)) +m.where(orderlineitem := OrderLineitem.to_identity(order_key=kse_tpch_sf1_lineitem.L_ORDERKEY, order_lineitem_number=kse_tpch_sf1_lineitem.L_LINENUMBER, unsafe=True)).define(orderlineitem.lineitem_quantity(kse_tpch_sf1_lineitem.L_QUANTITY)) +m.where(orderlineitem := OrderLineitem.to_identity(order_key=kse_tpch_sf1_lineitem.L_ORDERKEY, order_lineitem_number=kse_tpch_sf1_lineitem.L_LINENUMBER, unsafe=True)).define(orderlineitem.lineitem_extended_price(kse_tpch_sf1_lineitem.L_EXTENDEDPRICE)) +m.where(orderlineitem := OrderLineitem.to_identity(order_key=kse_tpch_sf1_lineitem.L_ORDERKEY, order_lineitem_number=kse_tpch_sf1_lineitem.L_LINENUMBER, unsafe=True)).define(orderlineitem.lineitem_discount(kse_tpch_sf1_lineitem.L_DISCOUNT)) +m.where(orderlineitem := OrderLineitem.to_identity(order_key=kse_tpch_sf1_lineitem.L_ORDERKEY, order_lineitem_number=kse_tpch_sf1_lineitem.L_LINENUMBER, unsafe=True)).define(orderlineitem.lineitem_tax(kse_tpch_sf1_lineitem.L_TAX)) +m.where(orderlineitem := OrderLineitem.to_identity(order_key=kse_tpch_sf1_lineitem.L_ORDERKEY, order_lineitem_number=kse_tpch_sf1_lineitem.L_LINENUMBER, unsafe=True)).define(orderlineitem.lineitem_comment(kse_tpch_sf1_lineitem.L_COMMENT)) +m.where(orderlineitem := OrderLineitem.to_identity(order_key=kse_tpch_sf1_lineitem.L_ORDERKEY, order_lineitem_number=kse_tpch_sf1_lineitem.L_LINENUMBER, unsafe=True)).define(orderlineitem.part_key(kse_tpch_sf1_lineitem.L_PARTKEY)) +m.where(orderlineitem := OrderLineitem.to_identity(order_key=kse_tpch_sf1_lineitem.L_ORDERKEY, order_lineitem_number=kse_tpch_sf1_lineitem.L_LINENUMBER, unsafe=True)).define(orderlineitem.supplier_key(kse_tpch_sf1_lineitem.L_SUPPKEY)) +m.where(orderlineitem := OrderLineitem.to_identity(order_key=kse_tpch_sf1_lineitem.L_ORDERKEY, order_lineitem_number=kse_tpch_sf1_lineitem.L_LINENUMBER, unsafe=True)).define(orderlineitem.lineitem_return_flag(kse_tpch_sf1_lineitem.L_RETURNFLAG)) +m.where(orderlineitem := OrderLineitem.to_identity(order_key=kse_tpch_sf1_lineitem.L_ORDERKEY, order_lineitem_number=kse_tpch_sf1_lineitem.L_LINENUMBER, unsafe=True)).define(orderlineitem.lineitem_status(kse_tpch_sf1_lineitem.L_LINESTATUS)) +m.where(orderlineitem := OrderLineitem.to_identity(order_key=kse_tpch_sf1_lineitem.L_ORDERKEY, order_lineitem_number=kse_tpch_sf1_lineitem.L_LINENUMBER, unsafe=True)).define(orderlineitem.lineitem_shipping_instructions(kse_tpch_sf1_lineitem.L_SHIPINSTRUCT)) +m.where(orderlineitem := OrderLineitem.to_identity(order_key=kse_tpch_sf1_lineitem.L_ORDERKEY, order_lineitem_number=kse_tpch_sf1_lineitem.L_LINENUMBER, unsafe=True)).define(orderlineitem.lineitem_shipping_mode(kse_tpch_sf1_lineitem.L_SHIPMODE)) +m.where(orderlineitem := OrderLineitem.to_identity(order_key=kse_tpch_sf1_lineitem.L_ORDERKEY, order_lineitem_number=kse_tpch_sf1_lineitem.L_LINENUMBER, unsafe=True)).define(orderlineitem.lineitem_ship_date(kse_tpch_sf1_lineitem.L_SHIPDATE)) +m.where(orderlineitem := OrderLineitem.to_identity(order_key=kse_tpch_sf1_lineitem.L_ORDERKEY, order_lineitem_number=kse_tpch_sf1_lineitem.L_LINENUMBER, unsafe=True)).define(orderlineitem.lineitem_commit_date(kse_tpch_sf1_lineitem.L_COMMITDATE)) +m.where(orderlineitem := OrderLineitem.to_identity(order_key=kse_tpch_sf1_lineitem.L_ORDERKEY, order_lineitem_number=kse_tpch_sf1_lineitem.L_LINENUMBER, unsafe=True)).define(orderlineitem.lineitem_receipt_date(kse_tpch_sf1_lineitem.L_RECEIPTDATE)) +m.define(Customer.new(customer_key=kse_tpch_sf1_customer.C_CUSTKEY)) +m.where(customer := Customer.to_identity(customer_key=kse_tpch_sf1_customer.C_CUSTKEY, unsafe=True)).define(customer.customer_name(kse_tpch_sf1_customer.C_NAME)) +m.where(customer := Customer.to_identity(customer_key=kse_tpch_sf1_customer.C_CUSTKEY, unsafe=True)).define(customer.customer_address(kse_tpch_sf1_customer.C_ADDRESS)) +m.where(customer := Customer.to_identity(customer_key=kse_tpch_sf1_customer.C_CUSTKEY, unsafe=True)).define(customer.customer_phone(kse_tpch_sf1_customer.C_PHONE)) +m.where(customer := Customer.to_identity(customer_key=kse_tpch_sf1_customer.C_CUSTKEY, unsafe=True)).define(customer.customer_account_balance(kse_tpch_sf1_customer.C_ACCTBAL)) +m.where(customer := Customer.to_identity(customer_key=kse_tpch_sf1_customer.C_CUSTKEY, unsafe=True)).define(customer.customer_market_segment(kse_tpch_sf1_customer.C_MKTSEGMENT)) +m.where(customer := Customer.to_identity(customer_key=kse_tpch_sf1_customer.C_CUSTKEY, unsafe=True)).define(customer.customer_comment(kse_tpch_sf1_customer.C_COMMENT)) +m.where(customer := Customer.to_identity(customer_key=kse_tpch_sf1_customer.C_CUSTKEY, unsafe=True)).define(customer.nation_key(kse_tpch_sf1_customer.C_NATIONKEY)) +m.where(Nation, Region, Nation.region_key == Region.region_key).define(Nation.nations_to_regions(Region)) +m.where(Supplier, Nation, Supplier.nation_key == Nation.nation_key).define(Supplier.suppliers_to_nations(Nation)) +m.where(PartSupplier, Part, PartSupplier.part_key == Part.part_key).define(PartSupplier.part_suppliers_to_part(Part)) +m.where(PartSupplier, Supplier, PartSupplier.supplier_key == Supplier.supplier_key).define(PartSupplier.part_suppliers_to_supplier(Supplier)) +m.where(CustOrder, Customer, CustOrder.customer_key == Customer.customer_key).define(CustOrder.orders_to_customers(Customer)) +m.where(OrderLineitem, CustOrder, OrderLineitem.order_key == CustOrder.order_key).define(OrderLineitem.lineitems_to_orders(CustOrder)) +m.where(OrderLineitem, PartSupplier, OrderLineitem.part_key == PartSupplier.part_key, OrderLineitem.supplier_key == PartSupplier.supplier_key).define(OrderLineitem.order_lineitems_to_part_suppliers(PartSupplier)) +m.where(Customer, Nation, Customer.nation_key == Nation.nation_key).define(Customer.customers_to_nations(Nation)) diff --git a/converters/ontology/tests/relationalai/snapshots/test_converter/test_palantir_export_converts_all_the_way_to_pyrel/palantir_chain.py b/converters/ontology/tests/relationalai/snapshots/test_converter/test_palantir_export_converts_all_the_way_to_pyrel/palantir_chain.py new file mode 100644 index 00000000..95db7868 --- /dev/null +++ b/converters/ontology/tests/relationalai/snapshots/test_converter/test_palantir_export_converts_all_the_way_to_pyrel/palantir_chain.py @@ -0,0 +1,41 @@ +# 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 relationalai.semantics import Model, String + +m = Model("model") + +Store = m.Concept("Store") +Sale = m.Concept("Sale") +palantir_palantir_sale_table = m.Table("PALANTIR.PALANTIR.SALE_TABLE", schema={'SALE_ID': String, 'AMOUNT': String, 'STORE_ID': String}) +palantir_palantir_store_table = m.Table("PALANTIR.PALANTIR.STORE_TABLE", schema={'STORE_ID': String, 'STORE_NAME': String}) + +Store.store_id = m.Property(f"{Store} store_id {String}") +Sale.sale_id = m.Property(f"{Sale} sale_id {String}") +Sale.amount = m.Property(f"{Sale} amount {String}") +Sale.store_id = m.Property(f"{Sale} store_id {String}") +Store.store_name = m.Property(f"{Store} store_name {String}") +Sale.sale_store = m.Property(f"{Sale} sale_store {Store}") + +Store.identify_by(Store.store_id) +Sale.identify_by(Sale.sale_id) +m.define(Sale.new(sale_id=palantir_palantir_sale_table.SALE_ID)) +m.where(sale := Sale.to_identity(sale_id=palantir_palantir_sale_table.SALE_ID, unsafe=True)).define(sale.amount(palantir_palantir_sale_table.AMOUNT)) +m.where(sale := Sale.to_identity(sale_id=palantir_palantir_sale_table.SALE_ID, unsafe=True)).define(sale.store_id(palantir_palantir_sale_table.STORE_ID)) +m.where(sale := Sale.to_identity(sale_id=palantir_palantir_sale_table.SALE_ID, unsafe=True), store := Store.lookup(store_id=palantir_palantir_sale_table.STORE_ID)).define(sale.sale_store(store)) +m.define(Store.new(store_id=palantir_palantir_store_table.STORE_ID)) +m.where(store := Store.to_identity(store_id=palantir_palantir_store_table.STORE_ID, unsafe=True)).define(store.store_name(palantir_palantir_store_table.STORE_NAME)) diff --git a/converters/ontology/tests/relationalai/test_converter.py b/converters/ontology/tests/relationalai/test_converter.py new file mode 100644 index 00000000..e8c13e52 --- /dev/null +++ b/converters/ontology/tests/relationalai/test_converter.py @@ -0,0 +1,300 @@ +# 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. + +"""Ossie -> RelationalAI: the converter's output, end to end. + +Each case parses an Ossie spec, converts it to an in-memory `OntologyModel`, and +decompiles the underlying pyrel model back to PyRel source. The snapshot is that +source — the code a user would otherwise have had to hand-write — so a converter +change shows up as a readable diff rather than a metamodel dump. + +Nothing here talks to an engine. `to_metamodel()` compiles in-process, and the +converter declares each dataset's columns from the Ossie spec rather than asking +a warehouse for them — so the tables the generated source names need not exist. +conftest pins `RAI_CONFIG_FILE_PATH` to an offline config. The whole module is +skipped when the optional `relationalai` extra is not installed: + + pip install -e ".[relationalai]" + +The snapshots are `.py`, because that is what they are: runnable PyRel source, +which `test_generated_pyrel_is_executable` below executes. pytest does not +collect them — it only imports files matching `test_*.py` — and `[tool.pyright]` +excludes `tests/snapshots`, since generated code that assigns attributes onto +`Concept` objects produces thousands of type errors that say nothing about the +converter. Keeping the real extension means an IDE highlights them, a reviewer +reads them as Python, and nothing has to be configured to make that work. +""" + +from __future__ import annotations + +from pathlib import Path + +import pytest + +from ossie_ontology.converter.palantir_to_ossie.converter import PalantirToOssieConverter +from ossie_ontology.parser import OssieParser +from ossie_ontology.vendor.palantir.parser import PalantirParser + +pyrel = pytest.importorskip( + "relationalai.semantics", + reason='needs the optional relationalai extra: pip install -e ".[relationalai]"', +) + +from relationalai.semantics.metamodel.pyrel_codegen import to_pyrel # noqa: E402 +from relationalai.semantics.metamodel.typer import Typer # noqa: E402 + +from ossie_ontology.converter.ossie_to_relationalai import ( # noqa: E402 + OntologyModel, + OssieToRelationalAIConverter, + declared_table, +) + + +# Specs under tests/fixtures/. Each name is the stem of `.yaml` and of the +# snapshot it produces. +from tests.conftest import SPEC_NAMES as SPECS + + +def convert(spec: str, specs_dir: Path) -> OntologyModel: + """Parse the named spec and convert it. + + `declared_table` rather than the default provider: it takes the column types + from the Ossie spec instead of reading them off the real table, so nothing + needs a connection and the tables the generated source names need not exist. + The default is the other way round for a good reason — only the table knows + how its identifiers were cased — which is exactly why these tests pick. + + No rows are supplied and none are needed; reading real data is an extension + (a downstream project supplies a CSV-backed provider), not something this + converter knows. + """ + # pyrel keeps constructed models in a process-wide registry; clearing it + # keeps each case independent and its generated names stable. + pyrel.Model.all_models.clear() + return OssieToRelationalAIConverter.convert( + OssieParser().parse(specs_dir / f"{spec}.yaml"), table_provider=declared_table + ) + + +def pyrel_source(spec: str, specs_dir: Path) -> str: + return to_pyrel(convert(spec, specs_dir).base_model().to_metamodel()) + + +@pytest.mark.parametrize("spec", SPECS) +def test_converted_model_decompiles_to_expected_pyrel(snapshot, specs_dir: Path, spec: str): + snapshot.assert_match(pyrel_source(spec, specs_dir), f"{spec}.py") + + +@pytest.mark.parametrize("spec", SPECS) +def test_generated_pyrel_is_executable(specs_dir: Path, spec: str): + """The generated source has to be runnable PyRel, not merely readable. + + Executes it in a fresh namespace and type-checks the model it builds, which + is what stops the snapshot above from silently locking in source that no one + could actually run. + """ + source = pyrel_source(spec, specs_dir) + + namespace: dict = {} + pyrel.Model.all_models.clear() + exec(compile(source, f"generated_{spec}.py", "exec"), namespace) + Typer().infer_model(namespace["m"].to_metamodel()) + + +def test_conversion_is_deterministic(specs_dir: Path): + """Two conversions of one spec produce identical source. + + The converter leans on dict iteration order throughout; if any of it ever + fell back to set or hash ordering, the snapshots above would start failing + intermittently rather than telling anyone why. + """ + assert pyrel_source("flights", specs_dir) == pyrel_source("flights", specs_dir) + + +def test_converter_result_is_an_ontology_model(specs_dir: Path): + """convert() returns the in-memory ontology, with its concepts resolvable.""" + om = convert("flights", specs_dir) + + assert isinstance(om, OntologyModel) + assert om.lookup_concept("Airport") is not None + assert om.lookup_concept("NoSuchConcept") is None + + +# --------------------------------------------------------------------------- +# The full spoke-to-spoke path: Palantir -> Ossie -> RelationalAI +# --------------------------------------------------------------------------- + +# An extracted Palantir export on disk, the same shape the real exports have: +# an ontology JSON plus a data_sets/ folder. Kept as files rather than built +# inline so it reads the way the Ossie fixtures do, and so it can be swapped for +# a real export without touching the test. +PALANTIR_SPEC = "retail_mini" + + +def test_palantir_export_converts_all_the_way_to_pyrel(snapshot, fixtures_dir: Path): + """Palantir JSON in, executable PyRel out, through the Ossie hub. + + Neither converter alone proves this works: `palantir_to_ossie` stops at the + Ossie model and `ossie_to_relationalai` starts from one. This is the only + test that runs a vendor format through the hub and out the other side, which + is the point of the hub-and-spoke arrangement. + """ + ontology = PalantirToOssieConverter().convert( + PalantirParser().parse(fixtures_dir / "palantir" / PALANTIR_SPEC) + ) + + pyrel.Model.all_models.clear() + om = OssieToRelationalAIConverter.convert(ontology, table_provider=declared_table) + + assert isinstance(om, OntologyModel) + # Both Palantir object types survived the round trip as concepts. + assert om.lookup_concept("Store") is not None + assert om.lookup_concept("Sale") is not None + + source = to_pyrel(om.base_model().to_metamodel()) + snapshot.assert_match(source, "palantir_chain.py") + + namespace: dict = {} + pyrel.Model.all_models.clear() + exec(compile(source, "generated_palantir_chain.py", "exec"), namespace) + Typer().infer_model(namespace["m"].to_metamodel()) + + +# --------------------------------------------------------------------------- +# Derived identifiers +# --------------------------------------------------------------------------- + +def test_a_derived_identifier_creates_the_entity(behaviours_dir: Path): + """A derived identifier must emit `Concept.new(...)`, not a bare fact. + + `_head` in the formula emitter picks between the two on + `reasoner.is_identifier_relationship`, which reads the reasoner's registry + of constructing roles. That registry was populated by nothing for a long + time, so the predicate answered False for every relationship and this rule + came out as `Product.sku(...)` — an assertion about a Product that no rule + creates. It type-checked and produced an empty result, so nothing caught it. + + No other fixture pairs `identify_by` with `derived_by`, which is why the + defect survived: this test is the only thing standing on that branch. + """ + pyrel.Model.all_models.clear() + ontology = OssieParser().parse(behaviours_dir / "derived_identifier.yaml") + om = OssieToRelationalAIConverter.convert(ontology, table_provider=declared_table) + source = to_pyrel(om.base_model().to_metamodel()) + + rules = [line for line in source.splitlines() if ".define(" in line] + assert len(rules) == 1, f"expected one derived rule, got: {rules}" + rule = rules[0] + + assert "Product.new(sku=" in rule, ( + "a derived identifier must create the entity; emitting a plain fact " + f"leaves nothing to attach it to. Got: {rule}" + ) + assert "define(Product.sku(" not in rule + + +def test_the_reasoner_sees_declared_identifiers(behaviours_dir: Path): + """The registry behind that decision is actually populated. + + `identify_by` is the only declaration of an identifier Ossie has, so the + reasoner takes the constructing roles straight from the concept rather than + waiting for a constraint producer that does not exist. + """ + from ossie_ontology.reasoner import OntologyReasoner + + ontology = OssieParser().parse(behaviours_dir / "derived_identifier.yaml") + reasoner = OntologyReasoner() + ontology.ontology.register(reasoner) + + identifiers = [ + rel for rel in ontology.ontology.relationships + if reasoner.is_identifier_relationship(rel) + ] + assert {rel.name for rel in identifiers} == {"sku", "legacyCode"} + + +# --------------------------------------------------------------------------- +# Reference schemes behind bare-expression mappings +# --------------------------------------------------------------------------- + +# The table variables the decompiler names the two datasets after, derived from +# their `source:` in the fixture. Spelled out so the assertions below can tell +# the STORES-driven rules from the SALES-driven ones. +STORES_TABLE = "db_schema_stores" +SALES_TABLE = "db_schema_sales" + + +def define_rules(source: str) -> list[str]: + return [line for line in source.splitlines() if ".define(" in line] + + +def test_bare_expression_mappings_resolve_the_ref_scheme(behaviours_dir: Path): + """Three call sites ask the reasoner which relationship identifies a concept. + + A mapping may give a bare `expression` instead of `referent_mappings`, and a + derived concept may extend nothing. In both shapes the converter knows the + value but not what to call it, so it asks `ref_scheme` for the identifying + relationship and uses its name as the keyword of `Concept.new(...)`: + + * `_build_entity_binding` — Store mapped by expression alone; + * `_apply_link_binding` — Sale -> soldAt -> Store, a bare-expression link + node with children, which needs a child binding scoped to the Store; + * `FormulaConverter._head` — LargeSale, derived and extending nothing. + + Every other fixture identifies entities through `referent_mappings` and + derives only subtypes, so all three branches ran without a test over them. + """ + pyrel.Model.all_models.clear() + ontology = OssieParser().parse(behaviours_dir / "ref_scheme.yaml") + source = to_pyrel( + OssieToRelationalAIConverter.convert(ontology, table_provider=declared_table) + .base_model().to_metamodel() + ) + defined = define_rules(source) + + # Site 1: the expression names no relationship, so `nr` can only have come + # from the ref scheme. Without it the converter cannot key the entity at all. + assert any("Store.new(nr=" in rule and STORES_TABLE in rule for rule in defined), ( + f"expected a Store creation rule keyed by its ref scheme, got: {defined}" + ) + + # Site 2: the grandchild property has to land on the Store the expression + # identifies. When the ref scheme comes back empty the converter falls back + # to the parent binding and the rule is emitted against the Sale instead — + # silently attaching a store's region to a sale. + region_rules = [rule for rule in defined if ".region(" in rule and SALES_TABLE in rule] + assert len(region_rules) == 1, f"expected one region rule off SALES, got: {region_rules}" + assert "store.region(" in region_rules[0], ( + f"region reached through soldAt must be scoped to the Store: {region_rules[0]}" + ) + + # ...and keyed by the identifier value, not by the entity. `binds_to` wants + # `Store(col)` and the ref-scheme keyword wants `col`; rendering once and + # using it for both keyed the Store by a Store — `Store.new(nr=Store(col))`, + # which the typer rejects with Expected 'StoreNr', got 'Store'. + keyed = [rule for rule in defined if "Store.new(nr=" in rule and SALES_TABLE in rule] + assert len(keyed) == 1, f"expected one Store creation rule off SALES, got: {keyed}" + assert f"Store.new(nr={SALES_TABLE}.STORENR)" in keyed[0], ( + f"the ref-scheme keyword takes the bare column, not the entity: {keyed[0]}" + ) + + # Site 3: a derived concept that extends nothing is created, not asserted — + # the same distinction test_a_derived_identifier_creates_the_entity draws, + # reached through the concept branch of `_head` rather than the relationship one. + assert any("LargeSale.new(nr=" in rule for rule in defined), ( + f"expected LargeSale to be created with its identifier, got: {defined}" + ) diff --git a/converters/ontology/tests/spec/__init__.py b/converters/ontology/tests/spec/__init__.py new file mode 100644 index 00000000..a0c0c155 --- /dev/null +++ b/converters/ontology/tests/spec/__init__.py @@ -0,0 +1,18 @@ +# 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. + +"""The Ossie spec: parsing one into a model, and rendering one back out.""" diff --git a/converters/ontology/tests/snapshots/test_flights_snapshot/test_flights_roundtrip_yaml_snapshot/flights_roundtrip.yaml b/converters/ontology/tests/spec/snapshots/test_corpus/test_roundtrip_yaml_snapshot/flights/roundtrip.yaml similarity index 100% rename from converters/ontology/tests/snapshots/test_flights_snapshot/test_flights_roundtrip_yaml_snapshot/flights_roundtrip.yaml rename to converters/ontology/tests/spec/snapshots/test_corpus/test_roundtrip_yaml_snapshot/flights/roundtrip.yaml diff --git a/converters/ontology/tests/spec/snapshots/test_corpus/test_roundtrip_yaml_snapshot/retail/roundtrip.yaml b/converters/ontology/tests/spec/snapshots/test_corpus/test_roundtrip_yaml_snapshot/retail/roundtrip.yaml new file mode 100644 index 00000000..b72fe69b --- /dev/null +++ b/converters/ontology/tests/spec/snapshots/test_corpus/test_roundtrip_yaml_snapshot/retail/roundtrip.yaml @@ -0,0 +1,1192 @@ +# 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. + +version: 0.2.0.dev0 +name: RETAIL_REFACTORED +description: Retail ontology with employees, stores, products, transactions, orders, payments, and weekly sales facts. +ontology: +- concept: Payment + type: EntityType + description: Order Payment. + identify_by: + - id + relationships: + - name: id + roles: + - concept: PaymentId + verbalizes: + - '{Payment} is identified by {PaymentId}' + multiplicity: OneToOne + - name: type + roles: + - concept: String + multiplicity: ManyToOne + - name: amount + roles: + - concept: Decimal + multiplicity: ManyToOne +- concept: CashPayment + type: EntityType + description: Payment made in cash. + extends: + - Payment + derived_by: + - Payment.type == 'cash' + relationships: + - name: cashReceived + roles: + - concept: Decimal + multiplicity: ManyToOne + - name: change + roles: + - concept: Decimal + multiplicity: ManyToOne +- concept: CardPayment + type: EntityType + description: Payment made by card. + extends: + - Payment + derived_by: + - Payment.type == 'card' + relationships: + - name: cardNumber + roles: + - concept: String + multiplicity: ManyToOne + - name: cardHolderName + roles: + - concept: String + multiplicity: ManyToOne +- concept: OrderLineItem + type: EntityType + description: LineItem in an Order. + identify_by: + - order + - nr + relationships: + - name: order + roles: + - concept: Order + verbalizes: + - '{OrderLineItem} belongs to {Order}' + multiplicity: ManyToOne + - name: nr + roles: + - concept: Integer + verbalizes: + - '{OrderLineItem} has line item number {Integer}' + multiplicity: ManyToOne + - name: lineitem_extended_price + description: Extended price for the line item. + roles: + - concept: Decimal + name: lineitem_extended_price + verbalizes: + - '{OrderLineItem} has extended price {Decimal:lineitem_extended_price}' + multiplicity: ManyToOne + - name: lineitem_discount + description: Discount factor applied to the line. + roles: + - concept: Decimal + name: lineitem_discount + verbalizes: + - '{OrderLineItem} has discount factor {Decimal:lineitem_discount}' + multiplicity: ManyToOne + - name: net_revenue + description: The net extended price of the order line after discount, but before tax. + roles: + - concept: Decimal + name: net_revenue + verbalizes: + - '{OrderLineItem} has net revenue {Decimal:net_revenue}' + multiplicity: ManyToOne + derived_by: + - extended_price == OrderLineItem.lineitem_extended_price AND discount == OrderLineItem.lineitem_discount AND net_revenue == extended_price * (1 - discount) + - name: discounted_amount + description: The amount paid after subtracting the discount from the extended price. + roles: + - concept: Amount + name: discounted_amount + verbalizes: + - '{OrderLineItem} has discounted amount {Amount:discounted_amount}' + multiplicity: ManyToOne + derived_by: + - discounted_amount == Amount(OrderLineItem.lineitem_extended_price - OrderLineItem.lineitem_discount) +- concept: Order + type: EntityType + description: Order in a Store. + identify_by: + - nr + relationships: + - name: nr + roles: + - concept: OrderNr + verbalizes: + - '{Order} is identified by {OrderNr}' + multiplicity: OneToOne + - name: orderDate + roles: + - concept: Date + verbalizes: + - '{Order} has {Date}' + multiplicity: ManyToOne +- concept: Transaction + type: EntityType + description: Transaction registered by a point-of-sale terminal. + identify_by: + - nr + relationships: + - name: nr + roles: + - concept: TransactionNr + verbalizes: + - '{Transaction} is identified by {TransactionNr}' + multiplicity: OneToOne + - name: registeredBy + roles: + - concept: Terminal + verbalizes: + - '{Transaction} is registered by {Terminal}' + multiplicity: ManyToOne + - name: returns + roles: + - concept: Item + - concept: Amount + verbalizes: + - '{Transaction} refunds {Item} for {Amount}' + multiplicity: ManyToOne + - name: sales + roles: + - concept: Item + - concept: Amount + verbalizes: + - '{Transaction} sells {Item} for {Amount}' + multiplicity: ManyToOne + - name: transactionTerminalStore + roles: + - concept: Terminal + - concept: Store + verbalizes: + - '{Transaction} registered at {Terminal} deployed to {Store}' + derived_by: + - Transaction.registeredBy(Terminal) AND Terminal.deployedTo(Store) + - name: storeNrViaBinaryChain + roles: + - concept: StoreNr + name: store_nr + verbalizes: + - '{Transaction} was registered at a terminal deployed to store {StoreNr:store_nr}' + multiplicity: ManyToOne + derived_by: + - store_nr == Transaction.registeredBy(Terminal).deployedTo.nr + - name: storeNrViaTernaryChain + roles: + - concept: StoreNr + name: store_nr + verbalizes: + - '{Transaction} occurred at store {StoreNr:store_nr}' + multiplicity: ManyToOne + derived_by: + - store_nr == Transaction.transactionTerminalStore(Terminal).nr + - name: transactionRegion + roles: + - concept: Region + verbalizes: + - '{Transaction} occurred in {Region}' + multiplicity: ManyToOne + derived_by: + - Transaction.registeredBy(Terminal) AND Terminal.deployedTo(Store) AND Store.storeToRegion(Region) + - name: transactionStoreManager + roles: + - concept: Employee + verbalizes: + - '{Transaction} associated with store manager {Employee}' + multiplicity: ManyToOne + derived_by: + - Transaction.registeredBy(Terminal) AND Terminal.deployedTo(Store) AND Store.storeToManager(Employee) +- concept: Terminal + type: EntityType + description: A point-of-sale terminal for conducting retail transactions. + identify_by: + - id + relationships: + - name: id + roles: + - concept: RegisterId + verbalizes: + - '{Terminal} is identified by {RegisterId}' + multiplicity: OneToOne + - name: deployedTo + roles: + - concept: Store + verbalizes: + - '{Terminal} is deployed to {Store}' + multiplicity: ManyToOne +- concept: Store + type: EntityType + description: Physical store that offers items for sale to the public. + identify_by: + - nr + relationships: + - name: nr + roles: + - concept: StoreNr + verbalizes: + - '{Store} is identified by {StoreNr}' + multiplicity: OneToOne + - name: storeToManager + roles: + - concept: Employee + verbalizes: + - '{Store} is managed by {Employee}' + multiplicity: ManyToOne + - name: storeToRegion + roles: + - concept: Region + verbalizes: + - '{Store} belongs to {Region}' + multiplicity: ManyToOne + - name: avgReturns + roles: + - concept: Item + - concept: Amount + name: amt + verbalizes: + - '{Store} reports returns of {Item} for average {Amount:amt}' + multiplicity: ManyToOne + derived_by: + - amt == AVG[Amount WHERE Transaction.registeredBy(Terminal) AND Transaction.returns(Transaction, Item, Amount) AND Terminal.deployedTo(Store) GROUP BY Item, Store] + - name: avgReturnsWithMarkup + roles: + - concept: Item + - concept: Amount + name: amt + verbalizes: + - '{Store} reports returns of {Item} for 10%-marked-up average {Amount:amt}' + multiplicity: ManyToOne + derived_by: + - amt == Amount(AVG[Amount WHERE Transaction.registeredBy(Terminal) AND Transaction.returns(Transaction, Item, Amount) AND Terminal.deployedTo(Store) GROUP BY Item, Store] * 1.1) +- concept: Region + type: EntityType + description: Region of a country. + identify_by: + - name + relationships: + - name: name + roles: + - concept: RegionName + verbalizes: + - '{Region} is identified by {RegionName}' + multiplicity: OneToOne +- concept: Class + type: EntityType + description: Class of the Product. + identify_by: + - nr + relationships: + - name: nr + roles: + - concept: ClassNr + verbalizes: + - '{Class} is identified by {ClassNr}' + multiplicity: OneToOne + - name: classToCategory + roles: + - concept: Category + verbalizes: + - '{Class} is part of {Category}' + multiplicity: ManyToOne + - name: classToLabel + roles: + - concept: ClassLabel + verbalizes: + - '{Class} has {ClassLabel}' + multiplicity: OneToOne +- concept: ClassLabel + type: EntityType + description: ClassLabel of the Product. + identify_by: + - name + relationships: + - name: name + roles: + - concept: ClassLabelName + verbalizes: + - '{ClassLabel} is identified by {ClassLabelName}' + multiplicity: OneToOne +- concept: Category + type: EntityType + description: Category of the Product. + identify_by: + - name + relationships: + - name: name + roles: + - concept: CategoryName + verbalizes: + - '{Category} is identified by {CategoryName}' + multiplicity: OneToOne +- concept: Item + type: EntityType + description: An item for sale at a retailer. + identify_by: + - upc + relationships: + - name: upc + roles: + - concept: UniversalProductCode + verbalizes: + - '{Item} is identified by {UniversalProductCode}' + multiplicity: OneToOne + - name: description + roles: + - concept: String + multiplicity: ManyToOne + - name: itemToClass + roles: + - concept: Class + verbalizes: + - '{Item} is grouped by {Class}' + multiplicity: ManyToOne + - name: weekly_sales + roles: + - concept: Store + - concept: Integer + - concept: Decimal + verbalizes: + - '{Item} sold at {Store} in week {Integer} for {Decimal}' + multiplicity: ManyToOne + - name: weekly_gross_sales + roles: + - concept: Store + - concept: Integer + - concept: Decimal + verbalizes: + - '{Item} sold at {Store} in week {Integer} for gross {Decimal}' + multiplicity: ManyToOne + - name: weekly_gross_returns + roles: + - concept: Store + - concept: Integer + - concept: Decimal + verbalizes: + - '{Item} returned at {Store} in week {Integer} for {Decimal}' + multiplicity: ManyToOne + - name: weekly_net_revenue + roles: + - concept: Store + - concept: Integer + - concept: Decimal + verbalizes: + - '{Item} sold at {Store} in week {Integer} for net {Decimal}' + multiplicity: ManyToOne + - name: item_store_sales + roles: + - concept: Store + - concept: Decimal + verbalizes: + - '{Item} sold at {Store} for total {Decimal}' + multiplicity: ManyToOne +- concept: Employee + type: EntityType + description: Employee of the company. + identify_by: + - nr + relationships: + - name: nr + roles: + - concept: EmployeeNr + verbalizes: + - '{Employee} is identified by {EmployeeNr}' + multiplicity: OneToOne + - name: reportsTo + roles: + - concept: Employee + name: mgr + verbalizes: + - '{Employee} reports to {Employee:mgr}' + multiplicity: ManyToOne + - name: firstName + roles: + - concept: String + multiplicity: ManyToOne + - name: lastName + roles: + - concept: String + multiplicity: ManyToOne +- concept: PermanentEmployee + type: EntityType + description: Employee with indefinite contract. + extends: + - Employee + relationships: + - name: salary + roles: + - concept: Integer + verbalizes: + - '{PermanentEmployee} has monthly salary {Integer}' + multiplicity: ManyToOne +- concept: Contractor + type: EntityType + description: Employee with temporary contract. + extends: + - Employee + relationships: + - name: hourlyRate + roles: + - concept: Integer + verbalizes: + - '{Contractor} is paid hourly {Integer}' + multiplicity: ManyToOne +- concept: PaymentId + type: ValueType + description: Unique id for a Payment. + extends: + - Integer +- concept: OrderNr + type: ValueType + description: Unique id for an Order. + extends: + - Integer +- concept: RegionName + type: ValueType + description: Unique name for a Region. + extends: + - String +- concept: ClassLabelName + type: ValueType + description: Unique name for a ClassLabel. + extends: + - String +- concept: CategoryName + type: ValueType + description: Unique name for a Category. + extends: + - String +- concept: ClassNr + type: ValueType + description: Unique number generated for Class. + extends: + - Integer +- concept: UniversalProductCode + type: ValueType + description: 12-digit number used for tracking trade items. + extends: + - Integer + requires: + - 0 < UniversalProductCode AND UniversalProductCode <= 999999999999 +- concept: TransactionNr + type: ValueType + description: Unique transaction number generated by a point-of-sale terminal. + extends: + - Integer + requires: + - TransactionNr > 0 +- concept: StoreNr + type: ValueType + description: Unique identifier for a physical store. + extends: + - Integer +- concept: RegisterId + type: ValueType + description: Unique identifier for point-of-sale terminals (cash registers). + extends: + - Integer + requires: + - RegisterId > 0 +- concept: EmployeeNr + type: ValueType + description: Unique company-assigned employee number. + extends: + - Integer + requires: + - EmployeeNr > 0 +- concept: Amount + type: ValueType + description: Amount of money exchanged in return for some item(s). + extends: + - Decimal +ontology_mappings: +- name: retail_refactored_mapping + semantic_model: + name: Retail refactored semantic model + datasets: + - name: EMPLOYEES + source: DB.SCHEMA.EMPLOYEES + fields: + - name: empNr + expression: + dialects: + - dialect: ANSI_SQL + expression: empNr + - name: lastName + expression: + dialects: + - dialect: ANSI_SQL + expression: lastName + - name: firstName + expression: + dialects: + - dialect: ANSI_SQL + expression: firstName + - name: mgr + expression: + dialects: + - dialect: ANSI_SQL + expression: mgr + - name: addr + expression: + dialects: + - dialect: ANSI_SQL + expression: addr + - name: city + expression: + dialects: + - dialect: ANSI_SQL + expression: city + - name: state + expression: + dialects: + - dialect: ANSI_SQL + expression: state + - name: zip + expression: + dialects: + - dialect: ANSI_SQL + expression: zip + - name: CONTRACTORS + source: DB.SCHEMA.CONTRACTORS + fields: + - name: empNr + expression: + dialects: + - dialect: ANSI_SQL + expression: empNr + - name: hourlyRate + expression: + dialects: + - dialect: ANSI_SQL + expression: hourlyRate + - name: PERMANENT_EMPLOYEES + source: DB.SCHEMA.PERMANENT_EMPLOYEES + fields: + - name: empNr + expression: + dialects: + - dialect: ANSI_SQL + expression: empNr + - name: salary + expression: + dialects: + - dialect: ANSI_SQL + expression: salary + - name: LOCATIONS + source: DB.SCHEMA.LOCATIONS + fields: + - name: storeNr + expression: + dialects: + - dialect: ANSI_SQL + expression: storeNr + - name: mgr + expression: + dialects: + - dialect: ANSI_SQL + expression: mgr + - name: region + expression: + dialects: + - dialect: ANSI_SQL + expression: region + - name: addr + expression: + dialects: + - dialect: ANSI_SQL + expression: addr + - name: city + expression: + dialects: + - dialect: ANSI_SQL + expression: city + - name: state + expression: + dialects: + - dialect: ANSI_SQL + expression: state + - name: zip + expression: + dialects: + - dialect: ANSI_SQL + expression: zip + - name: PRODUCTS + source: DB.SCHEMA.PRODUCTS + fields: + - name: upc + expression: + dialects: + - dialect: ANSI_SQL + expression: upc + - name: description + expression: + dialects: + - dialect: ANSI_SQL + expression: description + - name: classNr + expression: + dialects: + - dialect: ANSI_SQL + expression: classNr + - name: classLabel + expression: + dialects: + - dialect: ANSI_SQL + expression: classLabel + - name: category + expression: + dialects: + - dialect: ANSI_SQL + expression: category + - name: TRANSACTIONS + source: DB.SCHEMA.TRANSACTIONS + fields: + - name: txn + expression: + dialects: + - dialect: ANSI_SQL + expression: txn + - name: sku + expression: + dialects: + - dialect: ANSI_SQL + expression: sku + - name: terminal + expression: + dialects: + - dialect: ANSI_SQL + expression: terminal + - name: amt + expression: + dialects: + - dialect: ANSI_SQL + expression: amt + - name: refundYN + expression: + dialects: + - dialect: ANSI_SQL + expression: refundYN + - name: timestamp + expression: + dialects: + - dialect: ANSI_SQL + expression: timestamp + - name: TERMINALS + source: DB.SCHEMA.TERMINALS + fields: + - name: id + expression: + dialects: + - dialect: ANSI_SQL + expression: id + - name: store + expression: + dialects: + - dialect: ANSI_SQL + expression: store + - name: svcDate + expression: + dialects: + - dialect: ANSI_SQL + expression: svcDate + - name: LINEITEMS + source: DB.SCHEMA.LINEITEMS + fields: + - name: quantity + expression: + dialects: + - dialect: ANSI_SQL + expression: quantity + - name: extendedPrice + expression: + dialects: + - dialect: ANSI_SQL + expression: extendedPrice + - name: discount + expression: + dialects: + - dialect: ANSI_SQL + expression: discount + - name: tax + expression: + dialects: + - dialect: ANSI_SQL + expression: tax + - name: orderNr + expression: + dialects: + - dialect: ANSI_SQL + expression: orderNr + - name: lineItemNr + expression: + dialects: + - dialect: ANSI_SQL + expression: lineItemNr + - name: upc + expression: + dialects: + - dialect: ANSI_SQL + expression: upc + - name: status + expression: + dialects: + - dialect: ANSI_SQL + expression: status + - name: shipMode + expression: + dialects: + - dialect: ANSI_SQL + expression: shipMode + - name: shipDate + expression: + dialects: + - dialect: ANSI_SQL + expression: shipDate + - name: receiptDate + expression: + dialects: + - dialect: ANSI_SQL + expression: receiptDate + - name: ORDERS + source: DB.SCHEMA.ORDERS + fields: + - name: nr + expression: + dialects: + - dialect: ANSI_SQL + expression: nr + - name: store + expression: + dialects: + - dialect: ANSI_SQL + expression: store + - name: ordDate + expression: + dialects: + - dialect: ANSI_SQL + expression: ordDate + - name: PAYMENTS + source: DB.SCHEMA.PAYMENTS + fields: + - name: id + expression: + dialects: + - dialect: ANSI_SQL + expression: id + - name: orderNr + expression: + dialects: + - dialect: ANSI_SQL + expression: orderNr + - name: paymentDate + expression: + dialects: + - dialect: ANSI_SQL + expression: paymentDate + - name: amount + expression: + dialects: + - dialect: ANSI_SQL + expression: amount + - name: type + expression: + dialects: + - dialect: ANSI_SQL + expression: type + - name: cardNumber + expression: + dialects: + - dialect: ANSI_SQL + expression: cardNumber + - name: cardExpiry + expression: + dialects: + - dialect: ANSI_SQL + expression: cardExpiry + - name: cardHolderName + expression: + dialects: + - dialect: ANSI_SQL + expression: cardHolderName + - name: cashReceived + expression: + dialects: + - dialect: ANSI_SQL + expression: cashReceived + - name: RETAIL_FACTS + source: DB.SCHEMA.RETAIL_FACTS + fields: + - name: item_nr + expression: + dialects: + - dialect: ANSI_SQL + expression: item_nr + - name: store_nr + expression: + dialects: + - dialect: ANSI_SQL + expression: store_nr + - name: week_nr + expression: + dialects: + - dialect: ANSI_SQL + expression: week_nr + - name: sls + expression: + dialects: + - dialect: ANSI_SQL + expression: sls + - name: returns + expression: + dialects: + - dialect: ANSI_SQL + expression: returns + concept_mappings: + - concept: Employee + object_mappings: + - referent_mappings: + - relationship: nr + expression: EMPLOYEES.empNr + link_mappings: + - object_mapping: + referent_mappings: + - relationship: nr + expression: EMPLOYEES.empNr + children: + - object_mapping: + concept: Employee + referent_mappings: + - relationship: nr + expression: EMPLOYEES.mgr + relationship: reportsTo + - object_mapping: + concept: String + expression: EMPLOYEES.firstName + relationship: firstName + - object_mapping: + concept: String + expression: EMPLOYEES.lastName + relationship: lastName + - concept: Contractor + object_mappings: + - concept: Employee + referent_mappings: + - relationship: nr + expression: CONTRACTORS.empNr + link_mappings: + - object_mapping: + concept: Employee + referent_mappings: + - relationship: nr + expression: CONTRACTORS.empNr + children: + - object_mapping: + concept: Integer + expression: CONTRACTORS.hourlyRate + relationship: hourlyRate + - concept: PermanentEmployee + object_mappings: + - concept: Employee + referent_mappings: + - relationship: nr + expression: PERMANENT_EMPLOYEES.empNr + link_mappings: + - object_mapping: + concept: Employee + referent_mappings: + - relationship: nr + expression: PERMANENT_EMPLOYEES.empNr + children: + - object_mapping: + concept: Integer + expression: PERMANENT_EMPLOYEES.salary + relationship: salary + - concept: Item + object_mappings: + - referent_mappings: + - relationship: upc + expression: PRODUCTS.upc + link_mappings: + - object_mapping: + referent_mappings: + - relationship: upc + expression: PRODUCTS.upc + children: + - object_mapping: + concept: Class + referent_mappings: + - relationship: nr + expression: PRODUCTS.classNr + relationship: itemToClass + - object_mapping: + concept: String + expression: PRODUCTS.description + relationship: description + - object_mapping: + referent_mappings: + - relationship: upc + expression: RETAIL_FACTS.item_nr + children: + - object_mapping: + concept: Store + referent_mappings: + - relationship: nr + expression: RETAIL_FACTS.store_nr + children: + - object_mapping: + concept: Integer + expression: RETAIL_FACTS.week_nr + children: + - object_mapping: + concept: Decimal + expression: RETAIL_FACTS.sls + relationship: weekly_sales + - object_mapping: + concept: Decimal + expression: RETAIL_FACTS.sls + RETAIL_FACTS.returns + relationship: weekly_gross_sales + - object_mapping: + concept: Decimal + expression: RETAIL_FACTS.returns + relationship: weekly_gross_returns + - object_mapping: + concept: Decimal + expression: RETAIL_FACTS.sls * 0.9 - RETAIL_FACTS.returns * 1.1 + 100 + relationship: weekly_net_revenue + - object_mapping: + referent_mappings: + - relationship: upc + expression: RETAIL_FACTS.item_nr + children: + - object_mapping: + concept: Store + referent_mappings: + - relationship: nr + expression: RETAIL_FACTS.store_nr + children: + - object_mapping: + concept: Decimal + expression: SUM[RETAIL_FACTS.sls WHERE RETAIL_FACTS.sls > 0 GROUP BY RETAIL_FACTS.item_nr, RETAIL_FACTS.store_nr] + relationship: item_store_sales + - concept: Category + object_mappings: + - referent_mappings: + - relationship: name + expression: PRODUCTS.category + - concept: ClassLabel + object_mappings: + - referent_mappings: + - relationship: name + expression: PRODUCTS.classLabel + - concept: Class + object_mappings: + - referent_mappings: + - relationship: nr + expression: PRODUCTS.classNr + link_mappings: + - object_mapping: + referent_mappings: + - relationship: nr + expression: PRODUCTS.classNr + children: + - object_mapping: + concept: Category + referent_mappings: + - relationship: name + expression: PRODUCTS.category + relationship: classToCategory + - object_mapping: + concept: ClassLabel + referent_mappings: + - relationship: name + expression: PRODUCTS.classLabel + relationship: classToLabel + - concept: Region + object_mappings: + - referent_mappings: + - relationship: name + expression: LOCATIONS.region + - concept: Store + object_mappings: + - referent_mappings: + - relationship: nr + expression: LOCATIONS.storeNr + link_mappings: + - object_mapping: + referent_mappings: + - relationship: nr + expression: LOCATIONS.storeNr + children: + - object_mapping: + concept: Employee + referent_mappings: + - relationship: nr + expression: LOCATIONS.mgr + relationship: storeToManager + - object_mapping: + concept: Region + referent_mappings: + - relationship: name + expression: LOCATIONS.region + relationship: storeToRegion + - concept: Terminal + object_mappings: + - referent_mappings: + - relationship: id + expression: TERMINALS.id + link_mappings: + - object_mapping: + referent_mappings: + - relationship: id + expression: TERMINALS.id + children: + - object_mapping: + concept: Store + referent_mappings: + - relationship: nr + expression: TERMINALS.store + relationship: deployedTo + - concept: Transaction + object_mappings: + - referent_mappings: + - relationship: nr + expression: TRANSACTIONS.txn + link_mappings: + - object_mapping: + referent_mappings: + - relationship: nr + expression: TRANSACTIONS.txn + children: + - object_mapping: + concept: Terminal + referent_mappings: + - relationship: id + expression: TRANSACTIONS.terminal + relationship: registeredBy + - object_mapping: + concept: Item + referent_mappings: + - relationship: upc + expression: TRANSACTIONS.sku + children: + - object_mapping: + concept: Amount + expression: TRANSACTIONS.amt WHERE (TRANSACTIONS.refundYN == 'Y') + relationship: returns + - object_mapping: + concept: Amount + expression: TRANSACTIONS.amt WHERE (NOT TRANSACTIONS.refundYN == 'Y') + relationship: sales + - concept: Order + object_mappings: + - referent_mappings: + - relationship: nr + expression: ORDERS.nr + link_mappings: + - object_mapping: + referent_mappings: + - relationship: nr + expression: ORDERS.nr + children: + - object_mapping: + concept: Date + expression: ORDERS.ordDate + relationship: orderDate + - concept: OrderLineItem + object_mappings: + - referent_mappings: + - relationship: order + referent_mappings: + - relationship: nr + expression: LINEITEMS.orderNr + - relationship: nr + expression: LINEITEMS.lineItemNr + link_mappings: + - object_mapping: + referent_mappings: + - relationship: order + referent_mappings: + - relationship: nr + expression: LINEITEMS.orderNr + - relationship: nr + expression: LINEITEMS.lineItemNr + children: + - object_mapping: + concept: Decimal + expression: LINEITEMS.extendedPrice + relationship: lineitem_extended_price + - object_mapping: + concept: Decimal + expression: LINEITEMS.discount + relationship: lineitem_discount + - concept: Payment + object_mappings: + - referent_mappings: + - relationship: id + expression: PAYMENTS.id + link_mappings: + - object_mapping: + referent_mappings: + - relationship: id + expression: PAYMENTS.id + children: + - object_mapping: + concept: String + expression: PAYMENTS.type + relationship: type + - object_mapping: + concept: Decimal + expression: PAYMENTS.amount + relationship: amount + - concept: CardPayment + link_mappings: + - object_mapping: + referent_mappings: + - relationship: id + expression: PAYMENTS.id WHERE (PAYMENTS.type == 'card') + children: + - object_mapping: + concept: String + expression: PAYMENTS.cardNumber + relationship: cardNumber + - object_mapping: + concept: String + expression: PAYMENTS.cardHolderName + relationship: cardHolderName + - concept: CashPayment + link_mappings: + - object_mapping: + referent_mappings: + - relationship: id + expression: PAYMENTS.id WHERE (PAYMENTS.type == 'cash') + children: + - object_mapping: + concept: Decimal + expression: PAYMENTS.cashReceived + relationship: cashReceived + - object_mapping: + concept: Decimal + expression: PAYMENTS.cashReceived - PAYMENTS.amount + relationship: change diff --git a/converters/ontology/tests/spec/snapshots/test_corpus/test_roundtrip_yaml_snapshot/tpch/roundtrip.yaml b/converters/ontology/tests/spec/snapshots/test_corpus/test_roundtrip_yaml_snapshot/tpch/roundtrip.yaml new file mode 100644 index 00000000..8ed0e4d9 --- /dev/null +++ b/converters/ontology/tests/spec/snapshots/test_corpus/test_roundtrip_yaml_snapshot/tpch/roundtrip.yaml @@ -0,0 +1,1282 @@ +# 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. + +version: 0.2.0.dev0 +name: snow_tpch +description: TPC-H ontology with customers, orders, line items, parts, suppliers, nations, and regions, mapped to the KSE.TPCH_SF1 Snowflake schema. +ontology: +- concept: Customer + type: EntityType + description: The customers table contains details about individual customers, including their unique customer identifier, name, address, account balance, and market segment. It is used to analyze customer demographics, purchasing behavior, and segmentation. Join this table with orders and lineitem tables to track sales performance and customer order history. + identify_by: + - customer_key + relationships: + - name: customer_key + roles: + - concept: Integer + verbalizes: + - '{Customer} is identified by {Integer}' + multiplicity: OneToOne + - name: customer_address + description: Customer address + roles: + - concept: String + name: customer_address + verbalizes: + - '{Customer} has {String:customer_address}' + multiplicity: ManyToOne + - name: customer_phone + description: Customer phone number + roles: + - concept: String + name: customer_phone + verbalizes: + - '{Customer} has {String:customer_phone}' + multiplicity: ManyToOne + - name: customer_account_balance + description: Customer account balance + roles: + - concept: Decimal + name: customer_account_balance + verbalizes: + - '{Customer} has {Decimal:customer_account_balance}' + multiplicity: ManyToOne + - name: customer_comment + description: Free-form customer comment + roles: + - concept: String + name: customer_comment + verbalizes: + - '{Customer} has {String:customer_comment}' + multiplicity: ManyToOne + - name: nation_key + description: Foreign key from the nations table + roles: + - concept: Integer + name: nation_key + verbalizes: + - '{Customer} has {Integer:nation_key}' + multiplicity: ManyToOne + - name: customer_name + description: Name of the customer. + roles: + - concept: String + name: customer_name + verbalizes: + - '{Customer} has {String:customer_name}' + multiplicity: ManyToOne + - name: customer_market_segment + description: The market segment for this customer + roles: + - concept: String + name: customer_market_segment + verbalizes: + - '{Customer} has {String:customer_market_segment}' + multiplicity: ManyToOne + - name: automobile_market_segment + description: A filter to retrieve customers in the AUTOMOBILE market segment. + verbalizes: + - '{Customer} has automobile_market_segment filter' + - name: top_10_customers_by_higest_account_balance + description: A filter to retrieve the top 10 customers with the highest account balance. + verbalizes: + - '{Customer} has top_10_customers_by_higest_account_balance filter' + - name: customers_to_nations + description: A relationship that maps instances of Customer to instances of Nation + roles: + - concept: Nation + verbalizes: + - '{Customer} maps to {Nation}' + multiplicity: ManyToOne + derived_by: + - Customer.nation_key == Nation.nation_key +- concept: OrderLineitem + type: EntityType + description: The lineitems table contains details of individual items within orders, including part numbers, quantities, and discounts. + identify_by: + - order_key + - order_lineitem_number + relationships: + - name: order_key + roles: + - concept: Integer + verbalizes: + - '{OrderLineitem} is identified by order {Integer}' + multiplicity: ManyToOne + - name: order_lineitem_number + roles: + - concept: Integer + verbalizes: + - '{OrderLineitem} is identified by line {Integer}' + multiplicity: ManyToOne + - name: lineitem_quantity + description: Quantity ordered + roles: + - concept: Decimal + name: lineitem_quantity + verbalizes: + - '{OrderLineitem} has {Decimal:lineitem_quantity}' + multiplicity: ManyToOne + - name: lineitem_extended_price + description: Extended price for the line item + roles: + - concept: Decimal + name: lineitem_extended_price + verbalizes: + - '{OrderLineitem} has {Decimal:lineitem_extended_price}' + multiplicity: ManyToOne + - name: lineitem_discount + description: Discount factor applied to the line + roles: + - concept: Decimal + name: lineitem_discount + verbalizes: + - '{OrderLineitem} has {Decimal:lineitem_discount}' + multiplicity: ManyToOne + - name: lineitem_tax + description: Tax factor applied to the line + roles: + - concept: Decimal + name: lineitem_tax + verbalizes: + - '{OrderLineitem} has {Decimal:lineitem_tax}' + multiplicity: ManyToOne + - name: lineitem_comment + description: Free-form comment for the line + roles: + - concept: String + name: lineitem_comment + verbalizes: + - '{OrderLineitem} has {String:lineitem_comment}' + multiplicity: ManyToOne + - name: net_revenue + description: The net extended price of the order line after discount, but before tax. + roles: + - concept: Decimal + name: net_revenue + verbalizes: + - '{OrderLineitem} has {Decimal:net_revenue}' + multiplicity: ManyToOne + - name: gross_revenue + description: The net extended price of the order line after discount, but before tax. + roles: + - concept: Decimal + name: gross_revenue + verbalizes: + - '{OrderLineitem} has {Decimal:gross_revenue}' + multiplicity: ManyToOne + - name: part_key + description: Part Key + roles: + - concept: Integer + name: part_key + verbalizes: + - '{OrderLineitem} has {Integer:part_key}' + multiplicity: ManyToOne + - name: supplier_key + description: Supplier Key + roles: + - concept: Integer + name: supplier_key + verbalizes: + - '{OrderLineitem} has {Integer:supplier_key}' + multiplicity: ManyToOne + - name: lineitem_return_flag + description: Return Flag for Line Item + roles: + - concept: String + name: lineitem_return_flag + verbalizes: + - '{OrderLineitem} has {String:lineitem_return_flag}' + multiplicity: ManyToOne + - name: lineitem_status + description: Order Line Item Fulfillment Status + roles: + - concept: String + name: lineitem_status + verbalizes: + - '{OrderLineitem} has {String:lineitem_status}' + multiplicity: ManyToOne + - name: lineitem_shipping_instructions + description: Shipping Instructions + roles: + - concept: String + name: lineitem_shipping_instructions + verbalizes: + - '{OrderLineitem} has {String:lineitem_shipping_instructions}' + multiplicity: ManyToOne + - name: lineitem_shipping_mode + description: Order Lineitem Shipping Mode + roles: + - concept: String + name: lineitem_shipping_mode + verbalizes: + - '{OrderLineitem} has {String:lineitem_shipping_mode}' + multiplicity: ManyToOne + - name: lineitem_ship_date + description: Order line item Date of shipment + roles: + - concept: Date + name: lineitem_ship_date + verbalizes: + - '{OrderLineitem} has {Date:lineitem_ship_date}' + multiplicity: ManyToOne + - name: lineitem_commit_date + description: Order lineitem Date of Commitment + roles: + - concept: Date + name: lineitem_commit_date + verbalizes: + - '{OrderLineitem} has {Date:lineitem_commit_date}' + multiplicity: ManyToOne + - name: lineitem_receipt_date + description: Date of receipt for each order line item + roles: + - concept: Date + name: lineitem_receipt_date + verbalizes: + - '{OrderLineitem} has {Date:lineitem_receipt_date}' + multiplicity: ManyToOne + - name: lineitems_to_orders + description: A relationship that maps instances of OrderLineitem to instances of CustOrder + roles: + - concept: CustOrder + verbalizes: + - '{OrderLineitem} maps to {CustOrder}' + multiplicity: ManyToOne + derived_by: + - OrderLineitem.order_key == CustOrder.order_key + - name: order_lineitems_to_part_suppliers + description: A relationship that maps instances of OrderLineitem to instances of PartSupplier + roles: + - concept: PartSupplier + verbalizes: + - '{OrderLineitem} maps to {PartSupplier}' + multiplicity: ManyToOne + derived_by: + - OrderLineitem.part_key == PartSupplier.part_key AND OrderLineitem.supplier_key == PartSupplier.supplier_key +- concept: CustOrder + type: EntityType + description: The orders table contains information about customer orders, including the order date, priority, and status. + identify_by: + - order_key + relationships: + - name: order_key + roles: + - concept: Integer + verbalizes: + - '{CustOrder} is identified by {Integer}' + multiplicity: OneToOne + - name: order_gross_total + description: Order total price + roles: + - concept: Decimal + name: order_gross_total + verbalizes: + - '{CustOrder} has {Decimal:order_gross_total}' + multiplicity: ManyToOne + - name: order_clerk + description: Clerk handling the order + roles: + - concept: String + name: order_clerk + verbalizes: + - '{CustOrder} has {String:order_clerk}' + multiplicity: ManyToOne + - name: order_ship_priority + description: Shipping priority for the order + roles: + - concept: Decimal + name: order_ship_priority + verbalizes: + - '{CustOrder} has {Decimal:order_ship_priority}' + multiplicity: ManyToOne + - name: order_comment + description: Free-form order comment + roles: + - concept: String + name: order_comment + verbalizes: + - '{CustOrder} has {String:order_comment}' + multiplicity: ManyToOne + - name: customer_key + description: Foreign key from Customer Table + roles: + - concept: Integer + name: customer_key + verbalizes: + - '{CustOrder} has {Integer:customer_key}' + multiplicity: ManyToOne + - name: order_status + description: Order Status + roles: + - concept: String + name: order_status + verbalizes: + - '{CustOrder} has {String:order_status}' + multiplicity: ManyToOne + - name: order_priority + description: Priority level assigned to an order + roles: + - concept: String + name: order_priority + verbalizes: + - '{CustOrder} has {String:order_priority}' + multiplicity: ManyToOne + - name: order_date + description: Date of Order + roles: + - concept: Date + name: order_date + verbalizes: + - '{CustOrder} has {Date:order_date}' + multiplicity: ManyToOne + - name: Yesterday + description: A filter to restrict data to only yesterday. + verbalizes: + - '{CustOrder} has Yesterday filter' + - name: YTD + description: A filter to restrict data to only current fiscal year. + verbalizes: + - '{CustOrder} has YTD filter' + - name: orders_to_customers + description: A relationship that maps instances of CustOrder to instances of Customer + roles: + - concept: Customer + verbalizes: + - '{CustOrder} maps to {Customer}' + multiplicity: ManyToOne + derived_by: + - CustOrder.customer_key == Customer.customer_key +- concept: PartSupplier + type: EntityType + description: The partsupp table contains information linking parts to suppliers, including part keys, supplier keys, and supply costs. + identify_by: + - part_key + - supplier_key + relationships: + - name: part_key + roles: + - concept: Integer + verbalizes: + - '{PartSupplier} is identified by part {Integer}' + multiplicity: ManyToOne + - name: supplier_key + roles: + - concept: Integer + verbalizes: + - '{PartSupplier} is identified by supplier {Integer}' + multiplicity: ManyToOne + - name: available_quantity + description: Available quantity for this part-supplier pair + roles: + - concept: Decimal + name: available_quantity + verbalizes: + - '{PartSupplier} has {Decimal:available_quantity}' + multiplicity: ManyToOne + - name: part_supplier_supply_cost + description: Supply cost for this part-supplier pair + roles: + - concept: Decimal + name: part_supplier_supply_cost + verbalizes: + - '{PartSupplier} has {Decimal:part_supplier_supply_cost}' + multiplicity: ManyToOne + - name: part_supplier_comment + description: Free-form comment on the part-supplier record + roles: + - concept: String + name: part_supplier_comment + verbalizes: + - '{PartSupplier} has {String:part_supplier_comment}' + multiplicity: ManyToOne + - name: part_suppliers_to_part + description: A relationship that maps instances of PartSupplier to instances of Part + roles: + - concept: Part + verbalizes: + - '{PartSupplier} maps to {Part}' + multiplicity: ManyToOne + derived_by: + - PartSupplier.part_key == Part.part_key + - name: part_suppliers_to_supplier + description: A relationship that maps instances of PartSupplier to instances of Supplier + roles: + - concept: Supplier + verbalizes: + - '{PartSupplier} maps to {Supplier}' + multiplicity: ManyToOne + derived_by: + - PartSupplier.supplier_key == Supplier.supplier_key +- concept: Part + type: EntityType + description: The parts table contains details about parts that suppliers offer, including unique part keys, names, and types. A part could be supplied from multple suppliers. + identify_by: + - part_key + relationships: + - name: part_key + roles: + - concept: Integer + verbalizes: + - '{Part} is identified by {Integer}' + multiplicity: OneToOne + - name: part_retail_price + description: Part retail price + roles: + - concept: Decimal + name: part_retail_price + verbalizes: + - '{Part} has {Decimal:part_retail_price}' + multiplicity: ManyToOne + - name: part_comment + description: Free-form part comment + roles: + - concept: String + name: part_comment + verbalizes: + - '{Part} has {String:part_comment}' + multiplicity: ManyToOne + - name: part_name + description: name of the parts + roles: + - concept: String + name: part_name + verbalizes: + - '{Part} has {String:part_name}' + multiplicity: ManyToOne + - name: part_manufacturer_name + description: Manufacture name of this part + roles: + - concept: String + name: part_manufacturer_name + verbalizes: + - '{Part} has {String:part_manufacturer_name}' + multiplicity: ManyToOne + - name: part_brand + description: Brand Names of the part + roles: + - concept: String + name: part_brand + verbalizes: + - '{Part} has {String:part_brand}' + multiplicity: ManyToOne + - name: part_type + description: type of the part + roles: + - concept: String + name: part_type + verbalizes: + - '{Part} has {String:part_type}' + multiplicity: ManyToOne + - name: part_container + description: Container Types for each part + roles: + - concept: String + name: part_container + verbalizes: + - '{Part} has {String:part_container}' + multiplicity: ManyToOne + - name: part_size + description: Size of the part + roles: + - concept: Integer + name: part_size + verbalizes: + - '{Part} has {Integer:part_size}' + multiplicity: ManyToOne +- concept: Supplier + type: EntityType + description: The suppliers table contains details about suppliers, including unique supplier keys, supplier names, addresses, and associated nations. + identify_by: + - supplier_key + relationships: + - name: supplier_key + roles: + - concept: Integer + verbalizes: + - '{Supplier} is identified by {Integer}' + multiplicity: OneToOne + - name: supplier_address + description: Supplier address + roles: + - concept: String + name: supplier_address + verbalizes: + - '{Supplier} has {String:supplier_address}' + multiplicity: ManyToOne + - name: supplier_phone + description: Supplier phone number + roles: + - concept: String + name: supplier_phone + verbalizes: + - '{Supplier} has {String:supplier_phone}' + multiplicity: ManyToOne + - name: supplier_account_balance + description: Supplier account balance + roles: + - concept: Decimal + name: supplier_account_balance + verbalizes: + - '{Supplier} has {Decimal:supplier_account_balance}' + multiplicity: ManyToOne + - name: supplier_comment + description: Free-form supplier comment + roles: + - concept: String + name: supplier_comment + verbalizes: + - '{Supplier} has {String:supplier_comment}' + multiplicity: ManyToOne + - name: supplier_name + description: Name of the supplier + roles: + - concept: String + name: supplier_name + verbalizes: + - '{Supplier} has {String:supplier_name}' + multiplicity: ManyToOne + - name: nation_key + description: Foreign key from the nations table + roles: + - concept: Integer + name: nation_key + verbalizes: + - '{Supplier} has {Integer:nation_key}' + multiplicity: ManyToOne + - name: suppliers_to_nations + description: A relationship that maps instances of Supplier to instances of Nation + roles: + - concept: Nation + verbalizes: + - '{Supplier} maps to {Nation}' + multiplicity: ManyToOne + derived_by: + - Supplier.nation_key == Nation.nation_key +- concept: Nation + type: EntityType + description: The nations table contains details about various countries, including a unique identifier for each nation, its name, and a reference to the region it belongs to. Each nation is uniquely identified by N_NATIONKEY. + identify_by: + - nation_key + relationships: + - name: nation_key + roles: + - concept: Integer + verbalizes: + - '{Nation} is identified by {Integer}' + multiplicity: OneToOne + - name: nation_comment + description: Free-form nation comment + roles: + - concept: String + name: nation_comment + verbalizes: + - '{Nation} has {String:nation_comment}' + multiplicity: ManyToOne + - name: region_key + description: Foreign key from the regions table + roles: + - concept: Integer + name: region_key + verbalizes: + - '{Nation} has {Integer:region_key}' + multiplicity: ManyToOne + - name: nation_name + description: List of nations/countries + roles: + - concept: String + name: nation_name + verbalizes: + - '{Nation} has {String:nation_name}' + multiplicity: ManyToOne + - name: North_America + description: A filter to restrict data to only North American Continent + verbalizes: + - '{Nation} has North America filter' + - name: nations_to_regions + description: A relationship that maps instances of Nation to instances of Region + roles: + - concept: Region + verbalizes: + - '{Nation} maps to {Region}' + multiplicity: ManyToOne + derived_by: + - Nation.region_key == Region.region_key +- concept: Region + type: EntityType + description: The regions table contains details about various geographic regions, each uniquely identified by a region key. Each region can have multiple nations associated with it. There are a total of 5 regions. + identify_by: + - region_key + relationships: + - name: region_key + roles: + - concept: Integer + verbalizes: + - '{Region} is identified by {Integer}' + multiplicity: OneToOne + - name: region_comment + description: Free-form region comment + roles: + - concept: String + name: region_comment + verbalizes: + - '{Region} has {String:region_comment}' + multiplicity: ManyToOne + - name: region_name + description: Geographic region of the world. Name of the region. + roles: + - concept: String + name: region_name + verbalizes: + - '{Region} has {String:region_name}' + multiplicity: ManyToOne +ontology_mappings: +- name: snow_tpch_mapping + semantic_model: + name: TPC-H Snowflake semantic model + datasets: + - name: KSE_TPCH_SF1_CUSTOMER + source: KSE.TPCH_SF1.CUSTOMER + fields: + - name: C_CUSTKEY + expression: + dialects: + - dialect: ANSI_SQL + expression: C_CUSTKEY + - name: C_NAME + expression: + dialects: + - dialect: ANSI_SQL + expression: C_NAME + - name: C_ADDRESS + expression: + dialects: + - dialect: ANSI_SQL + expression: C_ADDRESS + - name: C_NATIONKEY + expression: + dialects: + - dialect: ANSI_SQL + expression: C_NATIONKEY + - name: C_PHONE + expression: + dialects: + - dialect: ANSI_SQL + expression: C_PHONE + - name: C_ACCTBAL + expression: + dialects: + - dialect: ANSI_SQL + expression: C_ACCTBAL + - name: C_MKTSEGMENT + expression: + dialects: + - dialect: ANSI_SQL + expression: C_MKTSEGMENT + - name: C_COMMENT + expression: + dialects: + - dialect: ANSI_SQL + expression: C_COMMENT + - name: KSE_TPCH_SF1_ORDERS + source: KSE.TPCH_SF1.ORDERS + fields: + - name: O_ORDERKEY + expression: + dialects: + - dialect: ANSI_SQL + expression: O_ORDERKEY + - name: O_CUSTKEY + expression: + dialects: + - dialect: ANSI_SQL + expression: O_CUSTKEY + - name: O_ORDERSTATUS + expression: + dialects: + - dialect: ANSI_SQL + expression: O_ORDERSTATUS + - name: O_TOTALPRICE + expression: + dialects: + - dialect: ANSI_SQL + expression: O_TOTALPRICE + - name: O_ORDERDATE + expression: + dialects: + - dialect: ANSI_SQL + expression: O_ORDERDATE + - name: O_ORDERPRIORITY + expression: + dialects: + - dialect: ANSI_SQL + expression: O_ORDERPRIORITY + - name: O_CLERK + expression: + dialects: + - dialect: ANSI_SQL + expression: O_CLERK + - name: O_SHIPPRIORITY + expression: + dialects: + - dialect: ANSI_SQL + expression: O_SHIPPRIORITY + - name: O_COMMENT + expression: + dialects: + - dialect: ANSI_SQL + expression: O_COMMENT + - name: KSE_TPCH_SF1_LINEITEM + source: KSE.TPCH_SF1.LINEITEM + fields: + - name: L_ORDERKEY + expression: + dialects: + - dialect: ANSI_SQL + expression: L_ORDERKEY + - name: L_PARTKEY + expression: + dialects: + - dialect: ANSI_SQL + expression: L_PARTKEY + - name: L_SUPPKEY + expression: + dialects: + - dialect: ANSI_SQL + expression: L_SUPPKEY + - name: L_LINENUMBER + expression: + dialects: + - dialect: ANSI_SQL + expression: L_LINENUMBER + - name: L_QUANTITY + expression: + dialects: + - dialect: ANSI_SQL + expression: L_QUANTITY + - name: L_EXTENDEDPRICE + expression: + dialects: + - dialect: ANSI_SQL + expression: L_EXTENDEDPRICE + - name: L_DISCOUNT + expression: + dialects: + - dialect: ANSI_SQL + expression: L_DISCOUNT + - name: L_TAX + expression: + dialects: + - dialect: ANSI_SQL + expression: L_TAX + - name: L_RETURNFLAG + expression: + dialects: + - dialect: ANSI_SQL + expression: L_RETURNFLAG + - name: L_LINESTATUS + expression: + dialects: + - dialect: ANSI_SQL + expression: L_LINESTATUS + - name: L_SHIPDATE + expression: + dialects: + - dialect: ANSI_SQL + expression: L_SHIPDATE + - name: L_COMMITDATE + expression: + dialects: + - dialect: ANSI_SQL + expression: L_COMMITDATE + - name: L_RECEIPTDATE + expression: + dialects: + - dialect: ANSI_SQL + expression: L_RECEIPTDATE + - name: L_SHIPINSTRUCT + expression: + dialects: + - dialect: ANSI_SQL + expression: L_SHIPINSTRUCT + - name: L_SHIPMODE + expression: + dialects: + - dialect: ANSI_SQL + expression: L_SHIPMODE + - name: L_COMMENT + expression: + dialects: + - dialect: ANSI_SQL + expression: L_COMMENT + - name: KSE_TPCH_SF1_PARTSUPP + source: KSE.TPCH_SF1.PARTSUPP + fields: + - name: PS_PARTKEY + expression: + dialects: + - dialect: ANSI_SQL + expression: PS_PARTKEY + - name: PS_SUPPKEY + expression: + dialects: + - dialect: ANSI_SQL + expression: PS_SUPPKEY + - name: PS_AVAILQTY + expression: + dialects: + - dialect: ANSI_SQL + expression: PS_AVAILQTY + - name: PS_SUPPLYCOST + expression: + dialects: + - dialect: ANSI_SQL + expression: PS_SUPPLYCOST + - name: PS_COMMENT + expression: + dialects: + - dialect: ANSI_SQL + expression: PS_COMMENT + - name: KSE_TPCH_SF1_PART + source: KSE.TPCH_SF1.PART + fields: + - name: P_PARTKEY + expression: + dialects: + - dialect: ANSI_SQL + expression: P_PARTKEY + - name: P_NAME + expression: + dialects: + - dialect: ANSI_SQL + expression: P_NAME + - name: P_MFGR + expression: + dialects: + - dialect: ANSI_SQL + expression: P_MFGR + - name: P_BRAND + expression: + dialects: + - dialect: ANSI_SQL + expression: P_BRAND + - name: P_TYPE + expression: + dialects: + - dialect: ANSI_SQL + expression: P_TYPE + - name: P_SIZE + expression: + dialects: + - dialect: ANSI_SQL + expression: P_SIZE + - name: P_CONTAINER + expression: + dialects: + - dialect: ANSI_SQL + expression: P_CONTAINER + - name: P_RETAILPRICE + expression: + dialects: + - dialect: ANSI_SQL + expression: P_RETAILPRICE + - name: P_COMMENT + expression: + dialects: + - dialect: ANSI_SQL + expression: P_COMMENT + - name: KSE_TPCH_SF1_SUPPLIER + source: KSE.TPCH_SF1.SUPPLIER + fields: + - name: S_SUPPKEY + expression: + dialects: + - dialect: ANSI_SQL + expression: S_SUPPKEY + - name: S_NAME + expression: + dialects: + - dialect: ANSI_SQL + expression: S_NAME + - name: S_ADDRESS + expression: + dialects: + - dialect: ANSI_SQL + expression: S_ADDRESS + - name: S_NATIONKEY + expression: + dialects: + - dialect: ANSI_SQL + expression: S_NATIONKEY + - name: S_PHONE + expression: + dialects: + - dialect: ANSI_SQL + expression: S_PHONE + - name: S_ACCTBAL + expression: + dialects: + - dialect: ANSI_SQL + expression: S_ACCTBAL + - name: S_COMMENT + expression: + dialects: + - dialect: ANSI_SQL + expression: S_COMMENT + - name: KSE_TPCH_SF1_NATION + source: KSE.TPCH_SF1.NATION + fields: + - name: N_NATIONKEY + expression: + dialects: + - dialect: ANSI_SQL + expression: N_NATIONKEY + - name: N_NAME + expression: + dialects: + - dialect: ANSI_SQL + expression: N_NAME + - name: N_REGIONKEY + expression: + dialects: + - dialect: ANSI_SQL + expression: N_REGIONKEY + - name: N_COMMENT + expression: + dialects: + - dialect: ANSI_SQL + expression: N_COMMENT + - name: KSE_TPCH_SF1_REGION + source: KSE.TPCH_SF1.REGION + fields: + - name: R_REGIONKEY + expression: + dialects: + - dialect: ANSI_SQL + expression: R_REGIONKEY + - name: R_NAME + expression: + dialects: + - dialect: ANSI_SQL + expression: R_NAME + - name: R_COMMENT + expression: + dialects: + - dialect: ANSI_SQL + expression: R_COMMENT + concept_mappings: + - concept: Region + object_mappings: + - referent_mappings: + - relationship: region_key + expression: KSE_TPCH_SF1_REGION.R_REGIONKEY + link_mappings: + - object_mapping: + referent_mappings: + - relationship: region_key + expression: KSE_TPCH_SF1_REGION.R_REGIONKEY + children: + - object_mapping: + concept: String + expression: KSE_TPCH_SF1_REGION.R_NAME + relationship: region_name + - object_mapping: + concept: String + expression: KSE_TPCH_SF1_REGION.R_COMMENT + relationship: region_comment + - concept: Nation + object_mappings: + - referent_mappings: + - relationship: nation_key + expression: KSE_TPCH_SF1_NATION.N_NATIONKEY + link_mappings: + - object_mapping: + referent_mappings: + - relationship: nation_key + expression: KSE_TPCH_SF1_NATION.N_NATIONKEY + children: + - object_mapping: + concept: String + expression: KSE_TPCH_SF1_NATION.N_NAME + relationship: nation_name + - object_mapping: + concept: String + expression: KSE_TPCH_SF1_NATION.N_COMMENT + relationship: nation_comment + - object_mapping: + concept: Integer + expression: KSE_TPCH_SF1_NATION.N_REGIONKEY + relationship: region_key + - concept: Supplier + object_mappings: + - referent_mappings: + - relationship: supplier_key + expression: KSE_TPCH_SF1_SUPPLIER.S_SUPPKEY + link_mappings: + - object_mapping: + referent_mappings: + - relationship: supplier_key + expression: KSE_TPCH_SF1_SUPPLIER.S_SUPPKEY + children: + - object_mapping: + concept: String + expression: KSE_TPCH_SF1_SUPPLIER.S_NAME + relationship: supplier_name + - object_mapping: + concept: String + expression: KSE_TPCH_SF1_SUPPLIER.S_ADDRESS + relationship: supplier_address + - object_mapping: + concept: String + expression: KSE_TPCH_SF1_SUPPLIER.S_PHONE + relationship: supplier_phone + - object_mapping: + concept: Decimal + expression: KSE_TPCH_SF1_SUPPLIER.S_ACCTBAL + relationship: supplier_account_balance + - object_mapping: + concept: String + expression: KSE_TPCH_SF1_SUPPLIER.S_COMMENT + relationship: supplier_comment + - object_mapping: + concept: Integer + expression: KSE_TPCH_SF1_SUPPLIER.S_NATIONKEY + relationship: nation_key + - concept: Part + object_mappings: + - referent_mappings: + - relationship: part_key + expression: KSE_TPCH_SF1_PART.P_PARTKEY + link_mappings: + - object_mapping: + referent_mappings: + - relationship: part_key + expression: KSE_TPCH_SF1_PART.P_PARTKEY + children: + - object_mapping: + concept: String + expression: KSE_TPCH_SF1_PART.P_NAME + relationship: part_name + - object_mapping: + concept: String + expression: KSE_TPCH_SF1_PART.P_MFGR + relationship: part_manufacturer_name + - object_mapping: + concept: String + expression: KSE_TPCH_SF1_PART.P_BRAND + relationship: part_brand + - object_mapping: + concept: String + expression: KSE_TPCH_SF1_PART.P_TYPE + relationship: part_type + - object_mapping: + concept: Integer + expression: KSE_TPCH_SF1_PART.P_SIZE + relationship: part_size + - object_mapping: + concept: String + expression: KSE_TPCH_SF1_PART.P_CONTAINER + relationship: part_container + - object_mapping: + concept: Decimal + expression: KSE_TPCH_SF1_PART.P_RETAILPRICE + relationship: part_retail_price + - object_mapping: + concept: String + expression: KSE_TPCH_SF1_PART.P_COMMENT + relationship: part_comment + - concept: PartSupplier + object_mappings: + - referent_mappings: + - relationship: part_key + expression: KSE_TPCH_SF1_PARTSUPP.PS_PARTKEY + - relationship: supplier_key + expression: KSE_TPCH_SF1_PARTSUPP.PS_SUPPKEY + link_mappings: + - object_mapping: + referent_mappings: + - relationship: part_key + expression: KSE_TPCH_SF1_PARTSUPP.PS_PARTKEY + - relationship: supplier_key + expression: KSE_TPCH_SF1_PARTSUPP.PS_SUPPKEY + children: + - object_mapping: + concept: Decimal + expression: KSE_TPCH_SF1_PARTSUPP.PS_AVAILQTY + relationship: available_quantity + - object_mapping: + concept: Decimal + expression: KSE_TPCH_SF1_PARTSUPP.PS_SUPPLYCOST + relationship: part_supplier_supply_cost + - object_mapping: + concept: String + expression: KSE_TPCH_SF1_PARTSUPP.PS_COMMENT + relationship: part_supplier_comment + - concept: CustOrder + object_mappings: + - referent_mappings: + - relationship: order_key + expression: KSE_TPCH_SF1_ORDERS.O_ORDERKEY + link_mappings: + - object_mapping: + referent_mappings: + - relationship: order_key + expression: KSE_TPCH_SF1_ORDERS.O_ORDERKEY + children: + - object_mapping: + concept: Decimal + expression: KSE_TPCH_SF1_ORDERS.O_TOTALPRICE + relationship: order_gross_total + - object_mapping: + concept: String + expression: KSE_TPCH_SF1_ORDERS.O_CLERK + relationship: order_clerk + - object_mapping: + concept: Decimal + expression: KSE_TPCH_SF1_ORDERS.O_SHIPPRIORITY + relationship: order_ship_priority + - object_mapping: + concept: String + expression: KSE_TPCH_SF1_ORDERS.O_COMMENT + relationship: order_comment + - object_mapping: + concept: Integer + expression: KSE_TPCH_SF1_ORDERS.O_CUSTKEY + relationship: customer_key + - object_mapping: + concept: String + expression: KSE_TPCH_SF1_ORDERS.O_ORDERSTATUS + relationship: order_status + - object_mapping: + concept: String + expression: KSE_TPCH_SF1_ORDERS.O_ORDERPRIORITY + relationship: order_priority + - object_mapping: + concept: Date + expression: KSE_TPCH_SF1_ORDERS.O_ORDERDATE + relationship: order_date + - concept: OrderLineitem + object_mappings: + - referent_mappings: + - relationship: order_key + expression: KSE_TPCH_SF1_LINEITEM.L_ORDERKEY + - relationship: order_lineitem_number + expression: KSE_TPCH_SF1_LINEITEM.L_LINENUMBER + link_mappings: + - object_mapping: + referent_mappings: + - relationship: order_key + expression: KSE_TPCH_SF1_LINEITEM.L_ORDERKEY + - relationship: order_lineitem_number + expression: KSE_TPCH_SF1_LINEITEM.L_LINENUMBER + children: + - object_mapping: + concept: Decimal + expression: KSE_TPCH_SF1_LINEITEM.L_QUANTITY + relationship: lineitem_quantity + - object_mapping: + concept: Decimal + expression: KSE_TPCH_SF1_LINEITEM.L_EXTENDEDPRICE + relationship: lineitem_extended_price + - object_mapping: + concept: Decimal + expression: KSE_TPCH_SF1_LINEITEM.L_DISCOUNT + relationship: lineitem_discount + - object_mapping: + concept: Decimal + expression: KSE_TPCH_SF1_LINEITEM.L_TAX + relationship: lineitem_tax + - object_mapping: + concept: String + expression: KSE_TPCH_SF1_LINEITEM.L_COMMENT + relationship: lineitem_comment + - object_mapping: + concept: Integer + expression: KSE_TPCH_SF1_LINEITEM.L_PARTKEY + relationship: part_key + - object_mapping: + concept: Integer + expression: KSE_TPCH_SF1_LINEITEM.L_SUPPKEY + relationship: supplier_key + - object_mapping: + concept: String + expression: KSE_TPCH_SF1_LINEITEM.L_RETURNFLAG + relationship: lineitem_return_flag + - object_mapping: + concept: String + expression: KSE_TPCH_SF1_LINEITEM.L_LINESTATUS + relationship: lineitem_status + - object_mapping: + concept: String + expression: KSE_TPCH_SF1_LINEITEM.L_SHIPINSTRUCT + relationship: lineitem_shipping_instructions + - object_mapping: + concept: String + expression: KSE_TPCH_SF1_LINEITEM.L_SHIPMODE + relationship: lineitem_shipping_mode + - object_mapping: + concept: Date + expression: KSE_TPCH_SF1_LINEITEM.L_SHIPDATE + relationship: lineitem_ship_date + - object_mapping: + concept: Date + expression: KSE_TPCH_SF1_LINEITEM.L_COMMITDATE + relationship: lineitem_commit_date + - object_mapping: + concept: Date + expression: KSE_TPCH_SF1_LINEITEM.L_RECEIPTDATE + relationship: lineitem_receipt_date + - concept: Customer + object_mappings: + - referent_mappings: + - relationship: customer_key + expression: KSE_TPCH_SF1_CUSTOMER.C_CUSTKEY + link_mappings: + - object_mapping: + referent_mappings: + - relationship: customer_key + expression: KSE_TPCH_SF1_CUSTOMER.C_CUSTKEY + children: + - object_mapping: + concept: String + expression: KSE_TPCH_SF1_CUSTOMER.C_NAME + relationship: customer_name + - object_mapping: + concept: String + expression: KSE_TPCH_SF1_CUSTOMER.C_ADDRESS + relationship: customer_address + - object_mapping: + concept: String + expression: KSE_TPCH_SF1_CUSTOMER.C_PHONE + relationship: customer_phone + - object_mapping: + concept: Decimal + expression: KSE_TPCH_SF1_CUSTOMER.C_ACCTBAL + relationship: customer_account_balance + - object_mapping: + concept: String + expression: KSE_TPCH_SF1_CUSTOMER.C_MKTSEGMENT + relationship: customer_market_segment + - object_mapping: + concept: String + expression: KSE_TPCH_SF1_CUSTOMER.C_COMMENT + relationship: customer_comment + - object_mapping: + concept: Integer + expression: KSE_TPCH_SF1_CUSTOMER.C_NATIONKEY + relationship: nation_key diff --git a/converters/ontology/tests/snapshots/test_flights_snapshot/test_flights_structure_snapshot/flights_structure.txt b/converters/ontology/tests/spec/snapshots/test_corpus/test_structure_snapshot/flights/structure.txt similarity index 100% rename from converters/ontology/tests/snapshots/test_flights_snapshot/test_flights_structure_snapshot/flights_structure.txt rename to converters/ontology/tests/spec/snapshots/test_corpus/test_structure_snapshot/flights/structure.txt diff --git a/converters/ontology/tests/spec/snapshots/test_corpus/test_structure_snapshot/retail/structure.txt b/converters/ontology/tests/spec/snapshots/test_corpus/test_structure_snapshot/retail/structure.txt new file mode 100644 index 00000000..66501e79 --- /dev/null +++ b/converters/ontology/tests/spec/snapshots/test_corpus/test_structure_snapshot/retail/structure.txt @@ -0,0 +1,137 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +name: RETAIL_REFACTORED +version: 0.2.0.dev0 +description: Retail ontology with employees, stores, products, transactions, orders, payments, and weekly sales facts. + +ontology requires: + +concepts: + Amount (VALUE_TYPE) + extends: Decimal + CardPayment (ENTITY_TYPE) + extends: Payment + CashPayment (ENTITY_TYPE) + extends: Payment + Category (ENTITY_TYPE) + identify_by: Category.name + CategoryName (VALUE_TYPE) + extends: String + Class (ENTITY_TYPE) + identify_by: Class.nr + ClassLabel (ENTITY_TYPE) + identify_by: ClassLabel.name + ClassLabelName (VALUE_TYPE) + extends: String + ClassNr (VALUE_TYPE) + extends: Integer + Contractor (ENTITY_TYPE) + extends: Employee + Employee (ENTITY_TYPE) + identify_by: Employee.nr + EmployeeNr (VALUE_TYPE) + extends: Integer + requires: EmployeeNr > 0 + Item (ENTITY_TYPE) + identify_by: Item.upc + Order (ENTITY_TYPE) + identify_by: Order.nr + OrderLineItem (ENTITY_TYPE) + identify_by: OrderLineItem.nr, OrderLineItem.order + OrderNr (VALUE_TYPE) + extends: Integer + Payment (ENTITY_TYPE) + identify_by: Payment.id + PaymentId (VALUE_TYPE) + extends: Integer + PermanentEmployee (ENTITY_TYPE) + extends: Employee + Region (ENTITY_TYPE) + identify_by: Region.name + RegionName (VALUE_TYPE) + extends: String + RegisterId (VALUE_TYPE) + extends: Integer + requires: RegisterId > 0 + Store (ENTITY_TYPE) + identify_by: Store.nr + StoreNr (VALUE_TYPE) + extends: Integer + Terminal (ENTITY_TYPE) + identify_by: Terminal.id + Transaction (ENTITY_TYPE) + identify_by: Transaction.nr + TransactionNr (VALUE_TYPE) + extends: Integer + requires: TransactionNr > 0 + UniversalProductCode (VALUE_TYPE) + extends: Integer + requires: 0 < UniversalProductCode AND UniversalProductCode <= 999999999999 + +relationships: + CardPayment.cardHolderName [MANY_TO_ONE]: CardPayment -> String + CardPayment.cardNumber [MANY_TO_ONE]: CardPayment -> String + CashPayment.cashReceived [MANY_TO_ONE]: CashPayment -> Decimal + CashPayment.change [MANY_TO_ONE]: CashPayment -> Decimal + Category.name [ONE_TO_ONE]: Category -> CategoryName + Class.classToCategory [MANY_TO_ONE]: Class -> Category + Class.classToLabel [ONE_TO_ONE]: Class -> ClassLabel + Class.nr [ONE_TO_ONE]: Class -> ClassNr + ClassLabel.name [ONE_TO_ONE]: ClassLabel -> ClassLabelName + Contractor.hourlyRate [MANY_TO_ONE]: Contractor -> Integer + Employee.firstName [MANY_TO_ONE]: Employee -> String + Employee.lastName [MANY_TO_ONE]: Employee -> String + Employee.nr [ONE_TO_ONE]: Employee -> EmployeeNr + Employee.reportsTo [MANY_TO_ONE]: Employee -> Employee + Item.description [MANY_TO_ONE]: Item -> String + Item.itemToClass [MANY_TO_ONE]: Item -> Class + Item.item_store_sales [MANY_TO_ONE]: Item -> Store -> Decimal + Item.upc [ONE_TO_ONE]: Item -> UniversalProductCode + Item.weekly_gross_returns [MANY_TO_ONE]: Item -> Store -> Integer -> Decimal + Item.weekly_gross_sales [MANY_TO_ONE]: Item -> Store -> Integer -> Decimal + Item.weekly_net_revenue [MANY_TO_ONE]: Item -> Store -> Integer -> Decimal + Item.weekly_sales [MANY_TO_ONE]: Item -> Store -> Integer -> Decimal + Order.nr [ONE_TO_ONE]: Order -> OrderNr + Order.orderDate [MANY_TO_ONE]: Order -> Date + OrderLineItem.discounted_amount [MANY_TO_ONE]: OrderLineItem -> Amount + OrderLineItem.lineitem_discount [MANY_TO_ONE]: OrderLineItem -> Decimal + OrderLineItem.lineitem_extended_price [MANY_TO_ONE]: OrderLineItem -> Decimal + OrderLineItem.net_revenue [MANY_TO_ONE]: OrderLineItem -> Decimal + OrderLineItem.nr [MANY_TO_ONE]: OrderLineItem -> Integer + OrderLineItem.order [MANY_TO_ONE]: OrderLineItem -> Order + Payment.amount [MANY_TO_ONE]: Payment -> Decimal + Payment.id [ONE_TO_ONE]: Payment -> PaymentId + Payment.type [MANY_TO_ONE]: Payment -> String + PermanentEmployee.salary [MANY_TO_ONE]: PermanentEmployee -> Integer + Region.name [ONE_TO_ONE]: Region -> RegionName + Store.avgReturns [MANY_TO_ONE]: Store -> Item -> Amount + Store.avgReturnsWithMarkup [MANY_TO_ONE]: Store -> Item -> Amount + Store.nr [ONE_TO_ONE]: Store -> StoreNr + Store.storeToManager [MANY_TO_ONE]: Store -> Employee + Store.storeToRegion [MANY_TO_ONE]: Store -> Region + Terminal.deployedTo [MANY_TO_ONE]: Terminal -> Store + Terminal.id [ONE_TO_ONE]: Terminal -> RegisterId + Transaction.nr [ONE_TO_ONE]: Transaction -> TransactionNr + Transaction.registeredBy [MANY_TO_ONE]: Transaction -> Terminal + Transaction.returns [MANY_TO_ONE]: Transaction -> Item -> Amount + Transaction.sales [MANY_TO_ONE]: Transaction -> Item -> Amount + Transaction.storeNrViaBinaryChain [MANY_TO_ONE]: Transaction -> StoreNr + Transaction.storeNrViaTernaryChain [MANY_TO_ONE]: Transaction -> StoreNr + Transaction.transactionRegion [MANY_TO_ONE]: Transaction -> Region + Transaction.transactionStoreManager [MANY_TO_ONE]: Transaction -> Employee + Transaction.transactionTerminalStore [None]: Transaction -> Terminal -> Store diff --git a/converters/ontology/tests/spec/snapshots/test_corpus/test_structure_snapshot/tpch/structure.txt b/converters/ontology/tests/spec/snapshots/test_corpus/test_structure_snapshot/tpch/structure.txt new file mode 100644 index 00000000..ded7e8e4 --- /dev/null +++ b/converters/ontology/tests/spec/snapshots/test_corpus/test_structure_snapshot/tpch/structure.txt @@ -0,0 +1,118 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +name: snow_tpch +version: 0.2.0.dev0 +description: TPC-H ontology with customers, orders, line items, parts, suppliers, nations, and regions, mapped to the KSE.TPCH_SF1 Snowflake schema. + +ontology requires: + +concepts: + CustOrder (ENTITY_TYPE) + identify_by: CustOrder.order_key + Customer (ENTITY_TYPE) + identify_by: Customer.customer_key + Nation (ENTITY_TYPE) + identify_by: Nation.nation_key + OrderLineitem (ENTITY_TYPE) + identify_by: OrderLineitem.order_key, OrderLineitem.order_lineitem_number + Part (ENTITY_TYPE) + identify_by: Part.part_key + PartSupplier (ENTITY_TYPE) + identify_by: PartSupplier.part_key, PartSupplier.supplier_key + Region (ENTITY_TYPE) + identify_by: Region.region_key + Supplier (ENTITY_TYPE) + identify_by: Supplier.supplier_key + +relationships: + CustOrder.YTD [None]: CustOrder + CustOrder.Yesterday [None]: CustOrder + CustOrder.customer_key [MANY_TO_ONE]: CustOrder -> Integer + CustOrder.order_clerk [MANY_TO_ONE]: CustOrder -> String + CustOrder.order_comment [MANY_TO_ONE]: CustOrder -> String + CustOrder.order_date [MANY_TO_ONE]: CustOrder -> Date + CustOrder.order_gross_total [MANY_TO_ONE]: CustOrder -> Decimal + CustOrder.order_key [ONE_TO_ONE]: CustOrder -> Integer + CustOrder.order_priority [MANY_TO_ONE]: CustOrder -> String + CustOrder.order_ship_priority [MANY_TO_ONE]: CustOrder -> Decimal + CustOrder.order_status [MANY_TO_ONE]: CustOrder -> String + CustOrder.orders_to_customers [MANY_TO_ONE]: CustOrder -> Customer + Customer.automobile_market_segment [None]: Customer + Customer.customer_account_balance [MANY_TO_ONE]: Customer -> Decimal + Customer.customer_address [MANY_TO_ONE]: Customer -> String + Customer.customer_comment [MANY_TO_ONE]: Customer -> String + Customer.customer_key [ONE_TO_ONE]: Customer -> Integer + Customer.customer_market_segment [MANY_TO_ONE]: Customer -> String + Customer.customer_name [MANY_TO_ONE]: Customer -> String + Customer.customer_phone [MANY_TO_ONE]: Customer -> String + Customer.customers_to_nations [MANY_TO_ONE]: Customer -> Nation + Customer.nation_key [MANY_TO_ONE]: Customer -> Integer + Customer.top_10_customers_by_higest_account_balance [None]: Customer + Nation.North_America [None]: Nation + Nation.nation_comment [MANY_TO_ONE]: Nation -> String + Nation.nation_key [ONE_TO_ONE]: Nation -> Integer + Nation.nation_name [MANY_TO_ONE]: Nation -> String + Nation.nations_to_regions [MANY_TO_ONE]: Nation -> Region + Nation.region_key [MANY_TO_ONE]: Nation -> Integer + OrderLineitem.gross_revenue [MANY_TO_ONE]: OrderLineitem -> Decimal + OrderLineitem.lineitem_comment [MANY_TO_ONE]: OrderLineitem -> String + OrderLineitem.lineitem_commit_date [MANY_TO_ONE]: OrderLineitem -> Date + OrderLineitem.lineitem_discount [MANY_TO_ONE]: OrderLineitem -> Decimal + OrderLineitem.lineitem_extended_price [MANY_TO_ONE]: OrderLineitem -> Decimal + OrderLineitem.lineitem_quantity [MANY_TO_ONE]: OrderLineitem -> Decimal + OrderLineitem.lineitem_receipt_date [MANY_TO_ONE]: OrderLineitem -> Date + OrderLineitem.lineitem_return_flag [MANY_TO_ONE]: OrderLineitem -> String + OrderLineitem.lineitem_ship_date [MANY_TO_ONE]: OrderLineitem -> Date + OrderLineitem.lineitem_shipping_instructions [MANY_TO_ONE]: OrderLineitem -> String + OrderLineitem.lineitem_shipping_mode [MANY_TO_ONE]: OrderLineitem -> String + OrderLineitem.lineitem_status [MANY_TO_ONE]: OrderLineitem -> String + OrderLineitem.lineitem_tax [MANY_TO_ONE]: OrderLineitem -> Decimal + OrderLineitem.lineitems_to_orders [MANY_TO_ONE]: OrderLineitem -> CustOrder + OrderLineitem.net_revenue [MANY_TO_ONE]: OrderLineitem -> Decimal + OrderLineitem.order_key [MANY_TO_ONE]: OrderLineitem -> Integer + OrderLineitem.order_lineitem_number [MANY_TO_ONE]: OrderLineitem -> Integer + OrderLineitem.order_lineitems_to_part_suppliers [MANY_TO_ONE]: OrderLineitem -> PartSupplier + OrderLineitem.part_key [MANY_TO_ONE]: OrderLineitem -> Integer + OrderLineitem.supplier_key [MANY_TO_ONE]: OrderLineitem -> Integer + Part.part_brand [MANY_TO_ONE]: Part -> String + Part.part_comment [MANY_TO_ONE]: Part -> String + Part.part_container [MANY_TO_ONE]: Part -> String + Part.part_key [ONE_TO_ONE]: Part -> Integer + Part.part_manufacturer_name [MANY_TO_ONE]: Part -> String + Part.part_name [MANY_TO_ONE]: Part -> String + Part.part_retail_price [MANY_TO_ONE]: Part -> Decimal + Part.part_size [MANY_TO_ONE]: Part -> Integer + Part.part_type [MANY_TO_ONE]: Part -> String + PartSupplier.available_quantity [MANY_TO_ONE]: PartSupplier -> Decimal + PartSupplier.part_key [MANY_TO_ONE]: PartSupplier -> Integer + PartSupplier.part_supplier_comment [MANY_TO_ONE]: PartSupplier -> String + PartSupplier.part_supplier_supply_cost [MANY_TO_ONE]: PartSupplier -> Decimal + PartSupplier.part_suppliers_to_part [MANY_TO_ONE]: PartSupplier -> Part + PartSupplier.part_suppliers_to_supplier [MANY_TO_ONE]: PartSupplier -> Supplier + PartSupplier.supplier_key [MANY_TO_ONE]: PartSupplier -> Integer + Region.region_comment [MANY_TO_ONE]: Region -> String + Region.region_key [ONE_TO_ONE]: Region -> Integer + Region.region_name [MANY_TO_ONE]: Region -> String + Supplier.nation_key [MANY_TO_ONE]: Supplier -> Integer + Supplier.supplier_account_balance [MANY_TO_ONE]: Supplier -> Decimal + Supplier.supplier_address [MANY_TO_ONE]: Supplier -> String + Supplier.supplier_comment [MANY_TO_ONE]: Supplier -> String + Supplier.supplier_key [ONE_TO_ONE]: Supplier -> Integer + Supplier.supplier_name [MANY_TO_ONE]: Supplier -> String + Supplier.supplier_phone [MANY_TO_ONE]: Supplier -> String + Supplier.suppliers_to_nations [MANY_TO_ONE]: Supplier -> Nation diff --git a/converters/ontology/tests/test_flights_snapshot.py b/converters/ontology/tests/spec/test_corpus.py similarity index 64% rename from converters/ontology/tests/test_flights_snapshot.py rename to converters/ontology/tests/spec/test_corpus.py index 6f31430c..34ffdca4 100644 --- a/converters/ontology/tests/test_flights_snapshot.py +++ b/converters/ontology/tests/spec/test_corpus.py @@ -15,21 +15,34 @@ # specific language governing permissions and limitations # under the License. -"""Snapshot tests for the flights ontology. +"""The corpus, pinned: every spec under fixtures/specs/, rendered two ways. -These lock in the converted structure and the round-tripped YAML so that any -change in parsing/conversion output shows up as a reviewable diff. +Two views of each ontology, both independent of any vendor: -Regenerate the snapshots after an intentional change with: + * the *structure* — concepts, identifiers, relationships and their + multiplicities, rendered as sorted text, so a parsing or conversion change + reads as a reviewable diff rather than a dump. + * the *round trip* — model back out to spec YAML through `OssieToSpecConverter`, + which is what catches a field that survives parsing but is dropped on the + way out. - pytest tests/test_flights_snapshot.py --snapshot-update +These are the breadth suite — they run over the whole corpus rather than +demonstrating one behaviour, which is what `test_parser.py` beside them does. + +Regenerate after an intentional change with: + + pytest tests/spec/test_corpus.py --snapshot-update """ from __future__ import annotations +import pytest + from ossie_ontology.converter.ossie_to_spec.converter import OssieToSpecConverter from ossie_ontology.model import OntologyComponent, OssieOntology +from tests.conftest import SPEC_NAMES + def _render_structure(model: OssieOntology) -> str: """Render a compact, deterministic text summary of the ontology structure.""" @@ -66,10 +79,12 @@ def _render_structure(model: OssieOntology) -> str: return "\n".join(lines) + "\n" -def test_flights_structure_snapshot(flights_model, snapshot): - snapshot.assert_match(_render_structure(flights_model), "flights_structure.txt") +@pytest.mark.parametrize("spec_model", SPEC_NAMES, indirect=True, ids=SPEC_NAMES) +def test_structure_snapshot(spec_model: OssieOntology, snapshot): + snapshot.assert_match(_render_structure(spec_model), "structure.txt") -def test_flights_roundtrip_yaml_snapshot(flights_model, snapshot): - spec = OssieToSpecConverter.convert(flights_model) - snapshot.assert_match(spec.dump_yaml(), "flights_roundtrip.yaml") \ No newline at end of file +@pytest.mark.parametrize("spec_model", SPEC_NAMES, indirect=True, ids=SPEC_NAMES) +def test_roundtrip_yaml_snapshot(spec_model: OssieOntology, snapshot): + spec = OssieToSpecConverter.convert(spec_model) + snapshot.assert_match(spec.dump_yaml(), "roundtrip.yaml") \ No newline at end of file diff --git a/converters/ontology/tests/test_ossie_parser.py b/converters/ontology/tests/spec/test_parser.py similarity index 73% rename from converters/ontology/tests/test_ossie_parser.py rename to converters/ontology/tests/spec/test_parser.py index f8e36e03..865d2e3f 100644 --- a/converters/ontology/tests/test_ossie_parser.py +++ b/converters/ontology/tests/spec/test_parser.py @@ -299,3 +299,92 @@ def test_relationships_are_grouped_under_their_own_container(): "Alpha": ["a1", "a2"], "Beta": ["b1", "b2"], } + + +# ----- dataset field types behind bare-expression mappings --------------- + +def test_bare_expression_on_an_entity_pins_the_identifier_type(behaviours_dir: Path): + """A key column holds what identifies the entity, not the entity. + + `_convert_referent_mapping` has always read the type off + `rel.last_role.player`. The object-mapping path used `om_spec.concept` + instead, which for an entity names the entity itself — so a column of store + numbers was typed `Store`, or, with no `concept:` to go on, left untyped and + declared `String`. Both reach the generated PyRel through `_pyrel_schema`, + where the typer rejects them against a `StoreNr`-typed identifier. + + `ref_scheme.yaml` maps Store both ways round: `STORES.storeNr` bare with no + concept, `SALES.storeNr` bare with `concept: Store`. Both must land on + `StoreNr`, the player of the relationship the reference scheme names. + """ + model = OssieParser().parse(behaviours_dir / "ref_scheme.yaml") + types = { + f"{dataset.name}.{field.name}": field.type.name if field.type else None + for mapping in model.ontology_mappings + for dataset in mapping.semantic_model.datasets + for field in dataset.fields + } + + assert types["STORES.storeNr"] == "StoreNr" + assert types["SALES.storeNr"] == "StoreNr" + # The value-type mappings are unaffected — they always named their own type. + assert types["SALES.saleNr"] == "SaleNr" + assert types["STORES.region"] == "Region" + assert types["SALES.amount"] == "Amount" + + +def test_one_column_can_key_an_entity_both_ways(behaviours_dir: Path): + """The two mapping styles agree on what a key column holds. + + They did not: a referent mapping pinned `StoreNr` and a bare expression + pinned `Store`, so using both on one column failed with "already mapped as + concept 'StoreNr' but this mapping expects 'Store'" — a conflict between two + spellings of the same fact, not a real disagreement in the spec. + """ + model = OssieParser().parse(behaviours_dir / "key_column_both_ways.yaml") + + sales = next( + dataset + for mapping in model.ontology_mappings + for dataset in mapping.semantic_model.datasets + if dataset.name == "SALES" + ) + store_nr = next(f for f in sales.fields if f.name == "storeNr") + assert store_nr.type is not None, ( + "the column was never pinned to a concept; both mappings should pin it" + ) + assert store_nr.type.name == "StoreNr" + + +def test_is_primitive_terminates_on_a_cyclic_extends_chain(): + """The property answers rather than exhausting the stack. + + `is_primitive` follows a chain of single-parent `extends` looking for a + builtin at the root. It used to recurse, so a hand-built cycle raised + RecursionError from a property — and from inside `OntologyReasoner`, which + asks it for every concept it ingests. No converter can produce a cycle + (`spec_to_ossie` rejects them, `palantir_to_ossie` breaks them), but + `Concept.extend()` is public API. + """ + a = Concept(name="A", type=ConceptType.ENTITY_TYPE) + b = Concept(name="B", type=ConceptType.ENTITY_TYPE, extends=[a]) + a.extend(b) # A extends B extends A + + # No builtin anywhere on the cycle, so neither is primitive. + assert a.is_primitive is False + assert b.is_primitive is False + + +def test_is_primitive_still_follows_a_chain_to_its_builtin_root(): + """The guard must not break the case the property exists for.""" + ontology = OntologyComponent() + string = ontology.ensure_builtin_concept("String") + assert string is not None + + code = Concept(name="Code", type=ConceptType.VALUE_TYPE, extends=[string]) + sku = Concept(name="Sku", type=ConceptType.VALUE_TYPE, extends=[code]) + + assert code.is_primitive is True + assert sku.is_primitive is True + # Two parents is not a chain, so not primitive — unchanged by the guard. + assert Concept(name="Pair", type=ConceptType.VALUE_TYPE, extends=[code, sku]).is_primitive is False diff --git a/converters/ontology/tests/test_examples_in_sync.py b/converters/ontology/tests/test_examples_in_sync.py index 724092b1..604020e9 100644 --- a/converters/ontology/tests/test_examples_in_sync.py +++ b/converters/ontology/tests/test_examples_in_sync.py @@ -29,7 +29,7 @@ import pytest -_FIXTURES_DIR = Path(__file__).resolve().parent / "fixtures" +_SPECS_DIR = Path(__file__).resolve().parent / "fixtures" / "specs" # tests/ -> ontology -> converters -> _EXAMPLES_DIR = Path(__file__).resolve().parents[3] / "examples" @@ -43,7 +43,7 @@ def test_vendored_input_matches_example(vendored_name: str, example_name: str): if not example_path.is_file(): pytest.skip(f"canonical example not present at {example_path}") - vendored_path = _FIXTURES_DIR / vendored_name + vendored_path = _SPECS_DIR / vendored_name assert vendored_path.is_file(), f"vendored input missing: {vendored_path}" assert vendored_path.read_text(encoding="utf-8") == example_path.read_text(encoding="utf-8"), ( f"'{vendored_path}' is out of sync with '{example_path}'. " diff --git a/converters/ontology/tests/test_offline.py b/converters/ontology/tests/test_offline.py new file mode 100644 index 00000000..fc9a205a --- /dev/null +++ b/converters/ontology/tests/test_offline.py @@ -0,0 +1,81 @@ +# 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. + +"""The suite runs offline, and says so if it stops being true. + +Two mechanisms, asserted here so neither can lapse quietly: + +* conftest pins ``RAI_CONFIG_FILE_PATH`` at import. This is not optional -- + ``relationalai`` resolves its config the moment a Model is constructed and + refuses to start without one -- so the question is never whether there is a + config, only whose. Unpinned, it searches the CWD, ``~/.rai``, ``~/.snowflake`` + and ``~/.dbt``, and on a contributor's machine it usually finds a real one. + +* an autouse fixture refuses every socket connection, so a test that reaches a + warehouse fails instead of succeeding for whoever has credentials. +""" + +from __future__ import annotations + +import os +import socket +from pathlib import Path + +import pytest +import yaml + + +def test_rai_config_is_pinned_to_the_offline_fixture(): + pinned = os.environ.get("RAI_CONFIG_FILE_PATH") + assert pinned, "conftest must pin RAI_CONFIG_FILE_PATH before any Model is built" + + path = Path(pinned) + assert path.is_file(), f"pinned config does not exist: {path}" + assert path.parent.name == "config" and path.parents[1].name == "tests", ( + f"the pinned config must be the one in tests/config/, not {path}" + ) + + +def test_the_offline_config_carries_no_real_credentials(): + """A placeholder that stopped being a placeholder would be a leak. + + The connection block exists only because the schema requires one. If someone + ever pastes a working account in to debug something, this fails before it is + committed. + """ + config = yaml.safe_load(Path(os.environ["RAI_CONFIG_FILE_PATH"]).read_text()) + + assert config.get("compiler", {}).get("dry_run") is True + for name, connection in config.get("connections", {}).items(): + account = str(connection.get("account", "")) + assert account == "not-a-real-account", ( + f"connection '{name}' names account {account!r}; the offline config " + "must never carry a reachable account" + ) + for field in ("user", "password", "warehouse", "role"): + assert connection.get(field) == "offline", ( + f"connection '{name}' sets {field} to something other than 'offline'" + ) + + +def test_the_network_guard_refuses_connections(): + """The guard itself works — otherwise the two tests above guard nothing.""" + with pytest.raises(RuntimeError, match="runs offline"): + socket.create_connection(("example.invalid", 80), timeout=1) + + with pytest.raises(RuntimeError, match="runs offline"): + socket.socket().connect(("example.invalid", 80)) diff --git a/converters/ontology/tests/test_optional_extra.py b/converters/ontology/tests/test_optional_extra.py new file mode 100644 index 00000000..d5aabb9c --- /dev/null +++ b/converters/ontology/tests/test_optional_extra.py @@ -0,0 +1,120 @@ +# 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. + +"""`relationalai` is an optional extra, and this is what holds that true. + +Everything except the Ossie -> RelationalAI converter must install and run +without the vendor SDK: parsing Ossie, converting Palantir, reading and writing +the spec. That only stays true while no module reachable from +`ossie_ontology/__init__.py` imports `relationalai` — an easy thing to break with +one convenient top-level import, and impossible to notice in a dev environment +where the extra is installed. + +The check runs in a subprocess with the SDK's distributions masked on +`sys.meta_path`. It has to be a subprocess: masking in-process would mean +re-importing `ossie_ontology` under a blocker, and the re-imported classes would +be different objects from the ones other tests already hold, breaking `isinstance` +for the rest of the session. +""" + +from __future__ import annotations + +import subprocess +import sys +import textwrap +from pathlib import Path + +# The distributions the `relationalai` extra pulls in. `pandas` and +# `more_itertools` are listed alongside the SDK because they arrive with it and +# are just as absent from a base install. +BLOCKED = ("relationalai", "pandas", "more_itertools") + +_SCRIPT = textwrap.dedent( + """ + import sys + from importlib.abc import MetaPathFinder + from pathlib import Path + + BLOCKED = {blocked!r} + + + class Missing(MetaPathFinder): + \"\"\"Answer every import of a blocked distribution the way a base install does.\"\"\" + + def find_spec(self, name, path=None, target=None): + if name.split(".")[0] in BLOCKED: + raise ImportError(f"No module named {{name!r}}") + return None + + + sys.meta_path.insert(0, Missing()) + + # The documented public surface, none of which may need the SDK. + from ossie_ontology import ( + FormulaParserFactory, + MappingFormulaParserFactory, + OntologyReasoner, + OssieParser, + OssieSpec, + OssieToSpecConverter, + PalantirParser, + PalantirToOssieConverter, + SpecToOssieConverter, + ) + + # Not just importable — a real spec has to parse, which exercises the ply + # grammar, the formula validator and the reasoner. + model = OssieParser().parse(Path(sys.argv[1])) + assert model.ontology.concepts(exclude_builtin=True), "spec parsed but produced no concepts" + OssieToSpecConverter.convert(model) + print("BASE_OK") + + try: + import ossie_ontology.converter.ossie_to_relationalai # noqa: F401 + except ImportError: + print("CONVERTER_GATED") + else: + print("CONVERTER_LEAKED") + """ +) + + +def _run_without_extra(spec: Path) -> subprocess.CompletedProcess: + return subprocess.run( + [sys.executable, "-c", _SCRIPT.format(blocked=BLOCKED), str(spec)], + capture_output=True, + text=True, + ) + + +def test_base_install_works_without_the_relationalai_extra(flights_path: Path): + """Parse and convert to spec with the SDK absent.""" + result = _run_without_extra(flights_path) + assert "BASE_OK" in result.stdout, ( + "the base surface needs the relationalai extra, which it must not:\n" + f"{result.stdout}\n{result.stderr}" + ) + + +def test_the_converter_is_the_only_thing_gated_on_the_extra(flights_path: Path): + """And the converter still requires it — otherwise the split is meaningless.""" + result = _run_without_extra(flights_path) + assert "CONVERTER_LEAKED" not in result.stdout, ( + "ossie_to_relationalai imported without the SDK; the extra is no longer " + "gating anything" + ) + assert "CONVERTER_GATED" in result.stdout, f"{result.stdout}\n{result.stderr}" diff --git a/converters/ontology/tests/test_palantir_converter.py b/converters/ontology/tests/test_palantir_converter.py deleted file mode 100644 index 3edd4980..00000000 --- a/converters/ontology/tests/test_palantir_converter.py +++ /dev/null @@ -1,1212 +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. - -"""Tests for what the Palantir converter admits into the model, and why. - -Two things decide it. The first is *status*: an export marks each object type, -property and relation as active, experimental, deprecated and so on, and only -some of those belong in a converted model. The defaults here suit an ontology in -production use — active work, plus the endorsed and intermediary entities it -leans on. They are class attributes read through ``allowed_*_statuses()`` -methods rather than inline checks, so a caller with a different definition of -"finished" (a draft ontology, say, where nearly everything is still -experimental) subclasses and widens them. These tests pin both the defaults and -the overriding. - -The second is *column matching*: an export's two halves routinely disagree on -the case of a physical column name, so a primary key naming ``LOCNO`` has to -find a dataset field called ``locno``. Exact matches still win. - -Each test builds the smallest export that shows the behaviour and runs it -through the real parser, so the fixtures stay in the JSON shapes an export -actually uses. -""" - -from __future__ import annotations - -import json -from pathlib import Path -from typing import Any, Iterable - -import pytest - -from ossie_ontology.converter.ossie_to_spec.converter import OssieToSpecConverter -from ossie_ontology.converter.palantir_to_ossie.converter import PalantirToOssieConverter -from ossie_ontology.external.palantir.model import Status -from ossie_ontology.external.palantir.parser import PalantirParser -from ossie_ontology.model import ConceptMapping, DatasetField, OssieOntology -from ossie_ontology.parser import OssieParser - - -# ----- export builders -------------------------------------------------- - -def _prop( - rid: str, - id_: str, - *, - status: str | None = None, - column: str | None = None, - pk_columns: dict[str, str] | None = None, - type_name: str = "STRING", -) -> dict[str, Any]: - prop: dict[str, Any] = {"rid": rid, "id": id_, "baseType": {"type": type_name}} - if status: - prop["status"] = {"type": status} - if column: - prop["column"] = column - if pk_columns: - # Keyed by the backing dataset's rid: a primary key can sit in a - # differently named column in each dataset that feeds the object type. - prop["primaryKeyMapping"] = { - ds_rid: {"columnName": name} for ds_rid, name in pk_columns.items() - } - return prop - - -def _object_type( - key: str, - name: str, - *, - status: str | None = None, - properties: Iterable[dict[str, Any]] = (), - primary_keys: Iterable[str] | None = None, - datasources: Iterable[str] | None = None, -) -> dict[str, Any]: - """An object type identified by ``_id``, plus any *properties* given. - - With no *datasources*, carries no ``datasources`` entry, so the parser falls - back to matching a dataset whose ``mainDatasetId`` is the object type's own - rid — which is what :func:`_dataset` builds. Pass dataset rids to back the - object type with several datasets instead. - """ - props = [_prop(f"ri.p.{key}_id", f"{key}_id"), *properties] - ot: dict[str, Any] = { - "rid": _ot_rid(key), - "id": key, - "displayName": name, - "properties": props, - "primaryKeys": list(primary_keys) if primary_keys is not None else [f"{key}_id"], - } - if status: - ot["status"] = {"type": status} - if datasources is not None: - ot["datasources"] = [ - {"datasourceRid": rid, "backingResourceRid": rid} for rid in datasources - ] - return ot - - -def _many_to_one( - key: str, - *, - one: str, - many: str, - key_map: dict[str, str], - status: str | None = None, -) -> dict[str, Any]: - """A M:1 relation from the *many* object type to the *one* object type. - - ``key_map`` maps a primary-key property rid on the one side to the property - rid holding it on the many side. - """ - relation: dict[str, Any] = { - "rid": f"ri.rel.{key}", - "id": key, - "definition": { - "type": "oneToMany", - "oneToMany": { - "objectTypeRidOneSide": _ot_rid(one), - "objectTypeRidManySide": _ot_rid(many), - "oneSidePrimaryKeyToManySidePropertyMapping": key_map, - }, - }, - } - if status: - relation["status"] = {"type": status} - return relation - - -def _many_to_many(key: str, *, a: str, b: str, status: str | None = None) -> dict[str, Any]: - """A M:M relation between object types *a* and *b*, keyed on their own ids. - - Each side's primary key maps to the like-named column of the join table, - which the relation carries as its backing datasource. - """ - relation: dict[str, Any] = { - "rid": f"ri.rel.{key}", - "id": key, - "definition": { - "type": "manyToMany", - "manyToMany": { - "objectTypeRidA": _ot_rid(a), - "objectTypeRidB": _ot_rid(b), - "objectTypeAPrimaryKeyPropertyMapping": {f"ri.p.{a}_id": f"{a}_id"}, - "objectTypeBPrimaryKeyPropertyMapping": {f"ri.p.{b}_id": f"{b}_id"}, - "joinTableDatasource": [ - {"datasourceRid": f"ri.ds.{key}", "backingResourceRid": f"ri.res.{key}"} - ], - }, - }, - } - if status: - relation["status"] = {"type": status} - return relation - - -def _intermediary( - key: str, - *, - a: str, - b: str, - via: str, - link_a: str, - link_b: str, - status: str | None = None, -) -> dict[str, Any]: - """An intermediary relation joining *a* to *b* through the *via* object type. - - ``link_a`` and ``link_b`` name the two M:1 relations it is derived from; - both must be present in the export or the parser rejects it. - """ - relation: dict[str, Any] = { - "rid": f"ri.rel.{key}", - "id": key, - "definition": { - "type": "intermediary", - "intermediary": { - "objectTypeRidA": _ot_rid(a), - "objectTypeRidB": _ot_rid(b), - "intermediaryObjectTypeRid": _ot_rid(via), - "aToIntermediaryLinkTypeRid": f"ri.rel.{link_a}", - "intermediaryToBLinkTypeRid": f"ri.rel.{link_b}", - }, - }, - } - if status: - relation["status"] = {"type": status} - return relation - - -def _dataset(key: str, columns: Iterable[str]) -> dict[str, Any]: - """The dataset backing object type *key*, with the columns named.""" - return { - "mainDatasetId": _ot_rid(key), - "datasetName": f"{key}_table", - "datasetSchema": [{"name": column, "type": "STRING"} for column in columns], - } - - -def _ot_rid(key: str) -> str: - return f"ri.ot.{key}" - - -def _convert( - tmp_path: Path, - object_types: Iterable[dict[str, Any]], - *, - relations: Iterable[dict[str, Any]] = (), - datasets: Iterable[dict[str, Any]] = (), - converter: type[PalantirToOssieConverter] = PalantirToOssieConverter, -) -> OssieOntology: - """Write an extracted-folder export, parse it, and convert it. - - Both files are rewritten on every call, so a test that converts the same - export under two different policies can call this twice. - """ - export = tmp_path / "export" - (export / "data_sets").mkdir(parents=True, exist_ok=True) - (export / "ontology.json").write_text( - json.dumps({"objectTypes": list(object_types), "relations": list(relations)}) - ) - (export / "data_sets" / "ds.json").write_text(json.dumps(list(datasets))) - return converter().convert(PalantirParser().parse(export)) - - -# ----- assertion helpers ------------------------------------------------ - -def _concept_names(model: OssieOntology) -> set[str]: - return {c.name for c in model.ontology.concepts(exclude_builtin=True)} - - -def _relationship_names(model: OssieOntology) -> set[str]: - return {r.full_name for r in model.ontology.relationships} - - -def _concept_mapping(model: OssieOntology, concept_name: str) -> ConceptMapping: - mappings = [ - cm - for om in model.ontology_mappings - for cm in om.concept_mappings - if cm.concept.name == concept_name - ] - assert len(mappings) == 1, f"expected one mapping for '{concept_name}', got {len(mappings)}" - return mappings[0] - - -def _mapped_relationship_names(model: OssieOntology, concept_name: str) -> set[str]: - """Names of the relationships a concept's mapping actually populates.""" - names = set() - for lm in _concept_mapping(model, concept_name).link_mappings: - for child in lm.children or []: - assert child.relationship is not None - names.add(child.relationship.full_name) - return names - - -def _identifier_columns(model: OssieOntology, concept_name: str) -> list[str]: - """The dataset fields a concept's mapping identifies its instances by.""" - [object_mapping] = _concept_mapping(model, concept_name).object_mappings - columns = [] - for rm in object_mapping.referent_mappings or []: - assert isinstance(rm.expression, DatasetField) - columns.append(rm.expression.name) - return columns - - -# ----- object type statuses --------------------------------------------- - -def test_default_policy_admits_active_endorsed_and_intermediary(tmp_path: Path): - model = _convert(tmp_path, [ - _object_type("alpha", "Alpha", status="active"), - _object_type("bravo", "Bravo", status="endorsed"), - _object_type("charlie", "Charlie", status="intermediary"), - _object_type("delta", "Delta", status="experimental"), - _object_type("echo", "Echo", status="deprecated"), - ]) - assert _concept_names(model) == {"Alpha", "Bravo", "Charlie"} - - -def test_object_type_named_like_a_builtin_is_reported(tmp_path: Path): - """`date` pascal-cases to `Date`, which is a builtin value type. - - Letting it through gives the entity the builtin's name, and every DATE - column in the export is then typed by this object type — so it is reported - as the concept-name collision it is, naming the object type rather than - surfacing later as a complaint about a missing 'date_id' property. - """ - object_types = [ - _object_type( - "date", - "date", - status="active", - properties=[_prop("ri.p.label", "label", column="label")], - ) - ] - - with pytest.raises(ValueError, match="which is a builtin value type"): - _convert(tmp_path, object_types, datasets=[_dataset("date", ["date_id", "label"])]) - - -def test_object_type_statuses_can_be_widened_by_a_subclass(tmp_path: Path): - """The extension point a draft-ontology converter is built on.""" - - class DraftConverter(PalantirToOssieConverter): - OBJECT_TYPE_STATUSES = PalantirToOssieConverter.OBJECT_TYPE_STATUSES | {Status.EXPERIMENTAL} - - object_types = [ - _object_type("alpha", "Alpha", status="active"), - _object_type("delta", "Delta", status="experimental"), - ] - assert _concept_names(_convert(tmp_path, object_types)) == {"Alpha"} - - widened = _convert(tmp_path, object_types, converter=DraftConverter) - assert _concept_names(widened) == {"Alpha", "Delta"} - - -# ----- property statuses ------------------------------------------------ - -def _widget_export() -> tuple[list[dict[str, Any]], list[dict[str, Any]]]: - """One object type with a property per status, and its backing dataset.""" - object_types = [ - _object_type( - "widget", - "Widget", - status="active", - properties=[ - _prop("ri.p.label", "label", status="active", column="label"), - _prop("ri.p.note", "note", status="experimental", column="note"), - _prop("ri.p.legacy", "legacy", status="deprecated", column="legacy"), - ], - ) - ] - return object_types, [_dataset("widget", ["widget_id", "label", "note", "legacy"])] - - -def test_experimental_property_is_declared_but_not_mapped(tmp_path: Path): - """An experimental property is part of the ontology, but nothing populates it. - - The two policies differ on purpose: the relationship is worth declaring - while the work is in progress, but pointing a mapping at a column that is - still being reshaped is not. - """ - object_types, datasets = _widget_export() - model = _convert(tmp_path, object_types, datasets=datasets) - - assert "Widget.note" in _relationship_names(model) - assert _mapped_relationship_names(model, "Widget") == {"Widget.label"} - - -def test_deprecated_property_is_not_converted_at_all(tmp_path: Path): - object_types, datasets = _widget_export() - model = _convert(tmp_path, object_types, datasets=datasets) - - assert "Widget.legacy" not in _relationship_names(model) - - -def test_mapping_property_statuses_can_be_widened_by_a_subclass(tmp_path: Path): - class DraftConverter(PalantirToOssieConverter): - MAPPING_PROPERTY_STATUSES = ( - PalantirToOssieConverter.MAPPING_PROPERTY_STATUSES | {Status.EXPERIMENTAL} - ) - - object_types, datasets = _widget_export() - model = _convert(tmp_path, object_types, datasets=datasets, converter=DraftConverter) - - assert _mapped_relationship_names(model, "Widget") == {"Widget.label", "Widget.note"} - - -# ----- property types --------------------------------------------------- - -def _role_player_types(model: OssieOntology) -> dict[str, str]: - """The concept each relationship's value role is typed by.""" - return {r.full_name: r.signature[-1].name for r in model.ontology.relationships} - - -def _field_types(model: OssieOntology) -> dict[str, str]: - """The concept each dataset field is typed by.""" - return { - field.name: field.type.name - for om in model.ontology_mappings - for dataset in om.semantic_model.datasets - for field in dataset.fields - if field.type is not None - } - - -def test_property_types_without_a_scalar_equivalent_are_not_numeric(tmp_path: Path): - """A blob or a reference must not come out typed as a number. - - Both the madlib role and the dataset field are typed from the same mapping, - so a wrong answer asserts arithmetic over an attachment column in two - places at once, with nothing downstream to reject it. - """ - types = { - "attachment": "ATTACHMENT", - "media_ref": "MEDIA_REFERENCE", - "secret": "CIPHER_TEXT", - "shape": "STRUCT", - "embedding": "VECTOR", - "whatever": "ANY", - "big": "LONG", - "small": "SHORT", - } - object_types = [ - _object_type( - "doc", - "Doc", - status="active", - properties=[ - _prop(f"ri.p.{name}", name, column=name, type_name=palantir_type) - for name, palantir_type in types.items() - ], - ) - ] - datasets = [ - { - "mainDatasetId": _ot_rid("doc"), - "datasetName": "doc_table", - "datasetSchema": [{"name": "doc_id", "type": "STRING"}] - + [{"name": name, "type": palantir_type} for name, palantir_type in types.items()], - } - ] - - model = _convert(tmp_path, object_types, datasets=datasets) - - expected = { - "attachment": "String", # an attachment rid - "media_ref": "String", # a media reference - "secret": "String", # ciphertext - "shape": "String", # no builtin describes a struct - "embedding": "String", # nor a vector - "whatever": "Any", # this one has an exact builtin - "big": "Integer", - "small": "Integer", - } - role_types = _role_player_types(model) - assert {name: role_types[f"Doc.{name}"] for name in expected} == expected - field_types = _field_types(model) - assert {name: field_types[name] for name in expected} == expected - - -def test_unrecognized_column_type_falls_back_to_string(tmp_path: Path): - """A warehouse type the enum has no name for must not fail the conversion. - - Column types drift with the export format, the field type is not serialized - downstream at all, and a column with no type already falls back to String — - so an unknown one is reported and carried, not raised. A bare ARRAY column - is still dropped, as before. - """ - object_types = [ - _object_type( - "doc", - "Doc", - status="active", - properties=[_prop("ri.p.blob", "blob", column="blob")], - ) - ] - datasets = [ - { - "mainDatasetId": _ot_rid("doc"), - "datasetName": "doc_table", - "datasetSchema": [ - {"name": "doc_id", "type": "STRING"}, - {"name": "blob", "type": "BINARY"}, - {"name": "props", "type": "MAP"}, - {"name": "tags", "type": "ARRAY"}, - {"name": "untyped"}, - ], - } - ] - - with pytest.warns(UserWarning, match="Unrecognized column type 'BINARY'"): - model = _convert(tmp_path, object_types, datasets=datasets) - - assert _field_types(model) == { - "doc_id": "String", - "blob": "String", - "props": "String", - "untyped": "String", # the pre-existing fallback this one now matches - } - # And the mapping is still built, rather than lost with the run. - assert _identifier_columns(model, "Doc") == ["doc_id"] - - -def test_column_name_becomes_an_identifier_and_keeps_reading_its_column(tmp_path: Path): - """A column name that is not an identifier has to be handled on both sides. - - The field *name* is what a mapping expression refers to, and it is matched - against an identifier pattern when the spec is read back — a name holding a - space fails that and is quietly taken for a formula rather than the field. - The field's *expression* is the SQL that reads the column, so it has to go - on naming the column exactly, quoted. - """ - columns = {"net amt": "net_amt", "a-b": "a_b", "x.y": "x_y", "9lives": "_9lives"} - object_types = [ - _object_type( - "doc", - "Doc", - status="active", - properties=[ - _prop(f"ri.p.p{i}", f"p{i}", column=column) - for i, column in enumerate(columns) - ], - ) - ] - datasets = [ - { - "mainDatasetId": _ot_rid("doc"), - "datasetName": "doc_table", - "datasetSchema": [{"name": "doc_id", "type": "STRING"}] - + [{"name": column, "type": "STRING"} for column in columns], - } - ] - - model = _convert(tmp_path, object_types, datasets=datasets) - - [dataset] = [ds for om in model.ontology_mappings for ds in om.semantic_model.datasets] - expressions = { - field.name: field.expression.dialects[0].expression - for field in dataset.fields - if field.expression is not None - } - assert set(expressions) == {"doc_id", *columns.values()} - assert expressions == { - "doc_id": "doc_id", # already an identifier, so left alone - "net_amt": '"net amt"', - "a_b": '"a-b"', - "x_y": '"x.y"', - "_9lives": '"9lives"', # a leading digit is not a bare identifier either - } - - -def test_field_name_survives_a_round_trip_as_a_field(tmp_path: Path): - """The reason the name matters: it has to resolve on the way back in.""" - object_types = [ - _object_type( - "doc", - "Doc", - status="active", - properties=[_prop("ri.p.net_amt", "net_amt", column="net amt")], - ) - ] - datasets = [ - { - "mainDatasetId": _ot_rid("doc"), - "datasetName": "doc_table", - "datasetSchema": [ - {"name": "doc_id", "type": "STRING"}, - {"name": "net amt", "type": "STRING"}, - ], - } - ] - model = _convert(tmp_path, object_types, datasets=datasets) - - roundtrip = tmp_path / "roundtrip.yaml" - roundtrip.write_text(OssieToSpecConverter.convert(model).dump_yaml()) - reparsed = OssieParser().parse(roundtrip) - - expressions = [ - child.object_mapping.expression - for om in reparsed.ontology_mappings - for cm in om.concept_mappings - for link_mapping in cm.link_mappings - for child in link_mapping.children or [] - ] - assert expressions, "the property mapping did not survive the round trip" - for expression in expressions: - assert isinstance(expression, DatasetField), f"demoted to {type(expression).__name__}" - - -def test_column_without_a_name_does_not_reach_the_converter(tmp_path: Path): - """The parser drops it; conversion proceeds on the columns that remain. - - This used to crash in `_normalize_field_name` on a `None` name, naming - neither the dataset nor the column. - """ - object_types = [ - _object_type( - "doc", - "Doc", - status="active", - properties=[_prop("ri.p.label", "label", column="label")], - ) - ] - datasets = [ - { - "mainDatasetId": _ot_rid("doc"), - "datasetName": "doc_table", - "datasetSchema": [ - {"name": "doc_id", "type": "STRING"}, - {"type": "STRING"}, - {"name": "label", "type": "STRING"}, - ], - } - ] - - with pytest.warns(UserWarning, match="Skipping column with missing name"): - model = _convert(tmp_path, object_types, datasets=datasets) - - assert set(_field_types(model)) == {"doc_id", "label"} - assert _mapped_relationship_names(model, "Doc") == {"Doc.label"} - - -# ----- relation statuses ------------------------------------------------ - -def _linked_export( - *, ot_status: str, relation_status: str -) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]: - """Two object types and the M:1 relation between them, at the given statuses.""" - object_types = [ - _object_type("widget", "Widget", status=ot_status), - _object_type( - "gadget", - "Gadget", - status=ot_status, - properties=[_prop("ri.p.gadget_widget_id", "gadget_widget_id", column="widget_id")], - ), - ] - relations = [ - _many_to_one( - "gadget_widget", - one="widget", - many="gadget", - key_map={"ri.p.widget_id": "ri.p.gadget_widget_id"}, - status=relation_status, - ) - ] - return object_types, relations - - -def test_relation_statuses_gate_conversion(tmp_path: Path): - object_types, relations = _linked_export(ot_status="active", relation_status="deprecated") - model = _convert(tmp_path, object_types, relations=relations) - - assert "Gadget.gadget_widget" not in _relationship_names(model) - - -def test_experimental_relation_between_active_object_types_is_converted(tmp_path: Path): - """Experimental relations ride in on their endpoints rather than their own status.""" - object_types, relations = _linked_export(ot_status="active", relation_status="experimental") - model = _convert(tmp_path, object_types, relations=relations) - - assert "Gadget.gadget_widget" in _relationship_names(model) - - -def test_experimental_relation_needs_its_endpoints_admitted_too(tmp_path: Path): - """Widening object types alone leaves their experimental relations behind. - - The endpoint check is separate from the object-type policy, so a converter - that admits experimental object types still drops every experimental - relation between them until it widens the endpoint statuses as well. - """ - - class WidenedObjectTypes(PalantirToOssieConverter): - OBJECT_TYPE_STATUSES = PalantirToOssieConverter.OBJECT_TYPE_STATUSES | {Status.EXPERIMENTAL} - - class WidenedEndpoints(WidenedObjectTypes): - RELATION_ENDPOINT_STATUSES = ( - PalantirToOssieConverter.RELATION_ENDPOINT_STATUSES | {Status.EXPERIMENTAL} - ) - - object_types, relations = _linked_export( - ot_status="experimental", relation_status="experimental" - ) - - partial = _convert(tmp_path, object_types, relations=relations, converter=WidenedObjectTypes) - assert _concept_names(partial) == {"Widget", "Gadget"} - assert "Gadget.gadget_widget" not in _relationship_names(partial) - - full = _convert(tmp_path, object_types, relations=relations, converter=WidenedEndpoints) - assert "Gadget.gadget_widget" in _relationship_names(full) - - -def test_link_colliding_with_a_property_name_is_skipped(tmp_path: Path): - """A shared name costs the link, not the conversion. - - Exports routinely name a link after the foreign-key column backing it, and - both become relationships on the same concept — which the ontology rejects - as a duplicate. The relation is dropped with a warning; the property, which - is converted first, keeps the name. - """ - object_types = [ - _object_type("widget", "Widget", status="active"), - _object_type( - "gadget", - "Gadget", - status="active", - properties=[_prop("ri.p.owner", "owner", column="owner")], - ), - ] - relations = [ - _many_to_one( - "owner", - one="widget", - many="gadget", - key_map={"ri.p.widget_id": "ri.p.owner"}, - status="active", - ) - ] - datasets = [_dataset("widget", ["widget_id"]), _dataset("gadget", ["gadget_id", "owner"])] - - with pytest.warns(UserWarning, match="Relation 'Gadget.owner' collides"): - model = _convert(tmp_path, object_types, relations=relations, datasets=datasets) - - # One 'owner' relationship, and it is the property's — still mapped to its - # own column, rather than the skipped link's walk into Widget. - assert _relationship_names(model) == {"Widget.widget_id", "Gadget.gadget_id", "Gadget.owner"} - assert _mapped_relationship_names(model, "Gadget") == {"Gadget.owner"} - - -# ----- many-to-many and intermediary relations -------------------------- - -def _tagging_export(*, widget_properties: Iterable[dict[str, Any]] = ()): - """Widget tagged with Tag through the WTag join object type. - - The two M:1 links run from WTag, which holds a foreign key to each side — - the ordinary join-table shape, and non-primary-key columns, so neither link - is read as inheritance. - """ - object_types = [ - _object_type("widget", "Widget", status="active", properties=list(widget_properties)), - _object_type("tag", "Tag", status="active"), - _object_type( - "wtag", - "WTag", - status="active", - properties=[ - _prop("ri.p.wtag_widget", "wtag_widget"), - _prop("ri.p.wtag_tag", "wtag_tag"), - ], - ), - ] - relations = [ - _many_to_one( - "a_link", one="widget", many="wtag", - key_map={"ri.p.widget_id": "ri.p.wtag_widget"}, status="active", - ), - _many_to_one( - "b_link", one="tag", many="wtag", - key_map={"ri.p.tag_id": "ri.p.wtag_tag"}, status="active", - ), - _intermediary( - "tagged", a="widget", b="tag", via="wtag", - link_a="a_link", link_b="b_link", status="active", - ), - ] - return object_types, relations - - -def _derived_by_expressions(model: OssieOntology, full_name: str) -> list[str]: - [relationship] = [r for r in model.ontology.relationships if r.full_name == full_name] - return [f.raw_expr for f in relationship.derived_by] - - -def test_many_to_many_relation_becomes_a_relationship_on_role_a(tmp_path: Path): - object_types = [ - _object_type("widget", "Widget", status="active"), - _object_type("tag", "Tag", status="active"), - ] - relations = [_many_to_many("tags", a="widget", b="tag", status="active")] - - model = _convert(tmp_path, object_types, relations=relations) - - assert "Widget.tags" in _relationship_names(model) - - -def test_intermediary_relation_is_derived_from_its_two_links(tmp_path: Path): - """The relationship lands on role A and is derived by joining both links.""" - object_types, relations = _tagging_export() - - model = _convert(tmp_path, object_types, relations=relations) - - names = _relationship_names(model) - assert "Widget.tagged" in names - assert {"WTag.a_link", "WTag.b_link"} <= names - assert _supertype_names(model, "WTag") == [] - assert _derived_by_expressions(model, "Widget.tagged") == [ - "WTag.a_link(Widget) AND WTag.b_link(Tag)" - ] - - -def test_many_to_many_link_colliding_with_a_property_name_is_skipped(tmp_path: Path): - """Same guard as the M:1 case, on the other relation kind.""" - object_types = [ - _object_type( - "widget", "Widget", status="active", - properties=[_prop("ri.p.tags", "tags", column="tags")], - ), - _object_type("tag", "Tag", status="active"), - ] - relations = [_many_to_many("tags", a="widget", b="tag", status="active")] - - with pytest.warns(UserWarning, match="Relation 'Widget.tags' collides"): - model = _convert(tmp_path, object_types, relations=relations) - - assert _relationship_names(model) == {"Widget.widget_id", "Widget.tags", "Tag.tag_id"} - - -def test_intermediary_link_colliding_with_a_property_name_is_skipped(tmp_path: Path): - """Skipping leaves no half-built relationship: the join formula goes too.""" - object_types, relations = _tagging_export( - widget_properties=[_prop("ri.p.tagged", "tagged", column="tagged")] - ) - - with pytest.warns(UserWarning, match="Relation 'Widget.tagged' collides"): - model = _convert(tmp_path, object_types, relations=relations) - - # The surviving 'Widget.tagged' is the property, so it carries no join. - assert _derived_by_expressions(model, "Widget.tagged") == [] - assert {"WTag.a_link", "WTag.b_link"} <= _relationship_names(model) - - -# ----- links across several datasets ------------------------------------ - -def _two_dataset_export(*, second_carries_primary_key: bool): - """Gadget backed by two datasets, each holding the FK to Widget. - - With *second_carries_primary_key* false, nothing in the second dataset - identifies a Gadget, so its ConceptMapping is dropped while the dataset — - and its copy of the foreign-key column — remains. - """ - object_types = [ - _object_type("widget", "Widget", status="active"), - _object_type( - "gadget", - "Gadget", - status="active", - properties=[_prop("ri.p.widget_ref", "widget_ref", column="widget_ref")], - datasources=["ri.ds.a", "ri.ds.b"], - ), - ] - relations = [ - _many_to_one( - "gadget_widget", - one="widget", - many="gadget", - key_map={"ri.p.widget_id": "ri.p.widget_ref"}, - status="active", - ) - ] - second_columns = ["gadget_id", "widget_ref"] if second_carries_primary_key else ["widget_ref"] - datasets = [ - { - "mainDatasetId": "ri.ds.a", - "datasetName": "ds_a", - "datasetSchema": [{"name": c, "type": "STRING"} for c in ("gadget_id", "widget_ref")], - }, - { - "mainDatasetId": "ri.ds.b", - "datasetName": "ds_b", - "datasetSchema": [{"name": c, "type": "STRING"} for c in second_columns], - }, - _dataset("widget", ["widget_id"]), - ] - return object_types, relations, datasets - - -def _link_dataset_pairs(model: OssieOntology, concept_name: str) -> set[tuple[str, str]]: - """For every attached link: (dataset identifying the mapping, dataset the - link's own expression comes from). The two must always be the same one.""" - def dataset_name(expression: Any) -> str: - assert isinstance(expression, DatasetField) - assert expression.dataset is not None - return expression.dataset.name - - pairs: set[tuple[str, str]] = set() - for om in model.ontology_mappings: - for cm in om.concept_mappings: - if cm.concept.name != concept_name: - continue - [root] = cm.object_mappings - identifying = {dataset_name(rm.expression) for rm in (root.referent_mappings or [])} - for link_mapping in cm.link_mappings: - for child in link_mapping.children or []: - for rm in child.object_mapping.referent_mappings or []: - pairs |= {(name, dataset_name(rm.expression)) for name in identifying} - return pairs - - -def test_link_is_attached_to_the_mapping_for_its_own_dataset(tmp_path: Path): - """Two datasets, two mappings, and each link reads its own dataset.""" - object_types, relations, datasets = _two_dataset_export(second_carries_primary_key=True) - - model = _convert(tmp_path, object_types, relations=relations, datasets=datasets) - - assert _link_dataset_pairs(model, "Gadget") == { - ("Gadget_ds_a", "Gadget_ds_a"), - ("Gadget_ds_b", "Gadget_ds_b"), - } - - -def test_link_is_not_attached_to_a_mapping_for_another_dataset(tmp_path: Path): - """A dataset whose mapping was dropped must not have its columns rehomed. - - The second dataset keeps the foreign-key column but loses its mapping, and - the link built from that column has nowhere to go — attaching it to the - first dataset's mapping would join two unrelated tables under one identity. - """ - object_types, relations, datasets = _two_dataset_export(second_carries_primary_key=False) - - with pytest.warns(UserWarning, match="cannot attach link 'Gadget.gadget_widget'"): - model = _convert(tmp_path, object_types, relations=relations, datasets=datasets) - - assert _link_dataset_pairs(model, "Gadget") == {("Gadget_ds_a", "Gadget_ds_a")} - - -# ----- inheritance ------------------------------------------------------ - -# Palantir has no subtype construct: inheritance appears as a M:1 relation whose -# property map pairs primary key to primary key, the one side being the subtype. -# Which of those the converter honours is the same status question as everywhere -# else, decided by allowed_subtype_relation_statuses() and the endpoint policy. - -def _subtype_export( - *, ot_status: str, relation_status: str -) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]: - """Gadget as a subtype of Widget, at the given statuses.""" - object_types = [ - _object_type("widget", "Widget", status=ot_status), - _object_type("gadget", "Gadget", status=ot_status), - ] - relations = [ - _many_to_one( - "gadget_widget", - one="gadget", - many="widget", - key_map={"ri.p.gadget_id": "ri.p.widget_id"}, - status=relation_status, - ) - ] - return object_types, relations - - -def _supertype_names(model: OssieOntology, concept_name: str) -> list[str]: - concept = model.ontology.lookup_concept(concept_name) - assert concept is not None, f"concept '{concept_name}' was not converted" - return [parent.name for parent in concept.extends] - - -def test_primary_key_to_primary_key_relation_becomes_inheritance(tmp_path: Path): - object_types, relations = _subtype_export(ot_status="active", relation_status="active") - model = _convert(tmp_path, object_types, relations=relations) - - assert _supertype_names(model, "Gadget") == ["Widget"] - # And only as inheritance: the relation is consumed, not also emitted as a - # reference from the supertype to the subtype. - assert "Widget.gadget_widget" not in _relationship_names(model) - - -def test_cycle_broken_subtype_relation_stays_an_ordinary_relationship(tmp_path: Path): - """The other half of the same rule: only inheritance that was actually - applied suppresses the relation. - - Two object types each declared a subtype of the other cannot both extend - the other, so the topological sort drops one edge. That relation is not - carried by any `extends`, so it has to come through as a reference — the - skip in `_convert_relationships` is keyed on what was consumed, not on - everything `_subtype_relations()` matched. - """ - object_types = [ - _object_type("widget", "Widget", status="active"), - _object_type("gadget", "Gadget", status="active"), - ] - relations = [ - _many_to_one( - "gadget_widget", - one="gadget", - many="widget", - key_map={"ri.p.gadget_id": "ri.p.widget_id"}, - status="active", - ), - _many_to_one( - "widget_gadget", - one="widget", - many="gadget", - key_map={"ri.p.widget_id": "ri.p.gadget_id"}, - status="active", - ), - ] - with pytest.warns(UserWarning, match="Cycle detected"): - model = _convert(tmp_path, object_types, relations=relations) - - assert _supertype_names(model, "Gadget") == ["Widget"] - assert _supertype_names(model, "Widget") == [] - names = _relationship_names(model) - assert "Widget.gadget_widget" not in names # consumed as the extends above - assert "Gadget.widget_gadget" in names # the dropped edge, as a reference - - -def test_relation_that_does_not_map_primary_keys_is_not_inheritance(tmp_path: Path): - """The structural half of the rule: a plain foreign key stays a reference.""" - object_types, relations = _linked_export(ot_status="active", relation_status="active") - model = _convert(tmp_path, object_types, relations=relations) - - assert _supertype_names(model, "Gadget") == [] - assert "Gadget.gadget_widget" in _relationship_names(model) - - -def test_partially_mapped_composite_key_is_not_inheritance(tmp_path: Path): - """Inheritance takes the parent's key whole, not in part. - - Widget is identified by (widget_id, wb) and the relation maps only - widget_id, so a Gadget cannot be identified as a Widget — that is an - ordinary foreign key. Reading it as inheritance instead reached - `_convert_mappings`, which resolves every parent primary key through the - property map, and died there on the unmapped `wb`. - """ - object_types = [ - _object_type( - "widget", - "Widget", - status="active", - properties=[_prop("ri.p.wb", "wb")], - primary_keys=["widget_id", "wb"], - ), - _object_type("gadget", "Gadget", status="active"), - ] - relations = [ - _many_to_one( - "gadget_widget", - one="gadget", - many="widget", - key_map={"ri.p.gadget_id": "ri.p.widget_id"}, - status="active", - ) - ] - datasets = [_dataset("widget", ["widget_id", "wb"]), _dataset("gadget", ["gadget_id"])] - - model = _convert(tmp_path, object_types, relations=relations, datasets=datasets) - - assert _supertype_names(model, "Gadget") == [] - assert "Widget.gadget_widget" in _relationship_names(model) - # Gadget keeps its own identity, and its mapping is built rather than crashing. - assert _identifier_columns(model, "Gadget") == ["gadget_id"] - - -def test_supertype_excluded_by_the_object_type_policy_is_reported(tmp_path: Path): - """The two status policies are independent, so they can disagree. - - A DEPRECATED parent joined to an ACTIVE child by an ACTIVE PK-to-PK relation - passes the subtype-relation gate and fails the object-type gate under the - defaults, so there is no parent concept to extend. That has to say which - policies disagree — as an error, not an assert, so `-O` fails here too - instead of building `Concept(extends=[None])` and dying later on a `None`. - """ - object_types = [ - _object_type("widget", "Widget", status="deprecated"), - _object_type("gadget", "Gadget", status="active"), - ] - relations = [ - _many_to_one( - "gadget_widget", - one="gadget", - many="widget", - key_map={"ri.p.gadget_id": "ri.p.widget_id"}, - status="active", - ) - ] - - with pytest.raises(ValueError, match="subtype of 'Widget'"): - _convert(tmp_path, object_types, relations=relations) - - -def test_deprecated_subtype_relation_is_not_read_as_inheritance(tmp_path: Path): - object_types, relations = _subtype_export(ot_status="active", relation_status="deprecated") - model = _convert(tmp_path, object_types, relations=relations) - - assert _supertype_names(model, "Gadget") == [] - - -def test_experimental_subtype_relation_between_active_object_types_is_read(tmp_path: Path): - """Same endpoint rule as any other experimental relation.""" - object_types, relations = _subtype_export(ot_status="active", relation_status="experimental") - model = _convert(tmp_path, object_types, relations=relations) - - assert _supertype_names(model, "Gadget") == ["Widget"] - - -def test_subtype_statuses_follow_the_converter_policy(tmp_path: Path): - """Inheritance keeps step with what the converter admits as a concept. - - Widening object types alone leaves an all-experimental hierarchy flat, for - the same reason it leaves experimental relations behind: the endpoints are - checked separately. - """ - - class WidenedObjectTypes(PalantirToOssieConverter): - OBJECT_TYPE_STATUSES = PalantirToOssieConverter.OBJECT_TYPE_STATUSES | {Status.EXPERIMENTAL} - - class WidenedEndpoints(WidenedObjectTypes): - RELATION_ENDPOINT_STATUSES = ( - PalantirToOssieConverter.RELATION_ENDPOINT_STATUSES | {Status.EXPERIMENTAL} - ) - - object_types, relations = _subtype_export( - ot_status="experimental", relation_status="experimental" - ) - - partial = _convert(tmp_path, object_types, relations=relations, converter=WidenedObjectTypes) - assert _supertype_names(partial, "Gadget") == [] - - full = _convert(tmp_path, object_types, relations=relations, converter=WidenedEndpoints) - assert _supertype_names(full, "Gadget") == ["Widget"] - - -def test_subtype_relation_statuses_can_be_widened_on_their_own(tmp_path: Path): - """The narrower default is a policy, not a fact about the export.""" - - class IntermediarySubtypes(PalantirToOssieConverter): - SUBTYPE_RELATION_STATUSES = ( - PalantirToOssieConverter.SUBTYPE_RELATION_STATUSES | {Status.INTERMEDIARY} - ) - - object_types, relations = _subtype_export(ot_status="active", relation_status="intermediary") - - assert _supertype_names(_convert(tmp_path, object_types, relations=relations), "Gadget") == [] - - widened = _convert( - tmp_path, object_types, relations=relations, converter=IntermediarySubtypes - ) - assert _supertype_names(widened, "Gadget") == ["Widget"] - - -# ----- primary key column matching -------------------------------------- - -def _location_export( - *, pk_column: str, dataset_columns: Iterable[str] -) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]: - """A Location whose primary key names *pk_column* in its backing dataset.""" - object_types = [ - { - "rid": _ot_rid("location"), - "id": "location", - "displayName": "Location", - "properties": [ - _prop("ri.p.locno", "locno", pk_columns={_ot_rid("location"): pk_column}) - ], - "primaryKeys": ["locno"], - } - ] - return object_types, [_dataset("location", dataset_columns)] - - -def test_composite_identifier_column_order_is_stable(tmp_path: Path): - """`primary_keys()` is a set of identity-hashed properties, so iterating it - yields a different order per run — which would reach the emitted YAML - through referent_mappings and make any diff of it noisy. - - Converting the same export repeatedly in one process is what exposes this: - a single conversion always looks fine. - """ - object_types = [ - _object_type( - "thing", - "Thing", - status="active", - properties=[_prop("ri.p.bee", "bee"), _prop("ri.p.aye", "aye")], - primary_keys=["thing_id", "bee", "aye"], - ) - ] - datasets = [_dataset("thing", ["thing_id", "bee", "aye"])] - - orderings = { - tuple(_identifier_columns(_convert(tmp_path, object_types, datasets=datasets), "Thing")) - for _ in range(20) - } - - assert orderings == {("aye", "bee", "thing_id")} - - -def test_primary_key_column_is_matched_case_insensitively(tmp_path: Path): - """`primaryKeyMapping` names the warehouse column, the dataset the API spelling.""" - object_types, datasets = _location_export(pk_column="LOCNO", dataset_columns=["locno"]) - model = _convert(tmp_path, object_types, datasets=datasets) - - assert _identifier_columns(model, "Location") == ["locno"] - - -def test_exact_primary_key_column_match_wins(tmp_path: Path): - """A case-sensitive source that carries both spellings keeps its own.""" - object_types, datasets = _location_export( - pk_column="LOCNO", dataset_columns=["locno", "LOCNO"] - ) - model = _convert(tmp_path, object_types, datasets=datasets) - - assert _identifier_columns(model, "Location") == ["LOCNO"] - - -def test_primary_key_column_absent_from_dataset_warns(tmp_path: Path): - """A column that is missing outright is still reported, not folded away.""" - object_types, datasets = _location_export(pk_column="MISSING", dataset_columns=["locno"]) - - with pytest.warns(UserWarning, match="does not contain a field named 'MISSING'"): - model = _convert(tmp_path, object_types, datasets=datasets) - - # Nothing identifies the concept, so the mapping is dropped along with it. - assert not [ - cm for om in model.ontology_mappings for cm in om.concept_mappings - ] diff --git a/converters/ontology/tests/test_reasoner.py b/converters/ontology/tests/test_reasoner.py new file mode 100644 index 00000000..92b21075 --- /dev/null +++ b/converters/ontology/tests/test_reasoner.py @@ -0,0 +1,188 @@ +# 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. + +"""OntologyReasoner, on its own. + +Nothing here needs the optional `relationalai` extra: the reasoner reads the +Ossie model and answers questions about it. The converter-side consequences of +those answers are asserted in tests/relationalai/test_converter.py. +""" + +from __future__ import annotations + +from pathlib import Path + +import pytest + +from ossie_ontology.model import Concept, ConceptType, OssieOntology +from ossie_ontology.parser import OssieParser +from ossie_ontology.reasoner import OntologyReasoner + + +@pytest.fixture +def reasoner_for(specs_dir: Path): + """Parse a fixture spec and return it with a reasoner registered on it.""" + + def build(spec: str) -> tuple[OssieOntology, OntologyReasoner]: + ontology = OssieParser().parse(specs_dir / f"{spec}.yaml") + reasoner = OntologyReasoner() + ontology.ontology.register(reasoner) + return ontology, reasoner + + return build + + +def names(scheme) -> tuple[str, ...] | None: + return None if scheme is None else tuple(rel.full_name for rel in scheme) + + +# --------------------------------------------------------------------------- +# ref_scheme +# --------------------------------------------------------------------------- + +def test_ref_scheme_returns_a_concepts_own_identifiers(reasoner_for): + """The declared `identify_by`, in declaration order.""" + ontology, reasoner = reasoner_for("retail") + employee = ontology.ontology.lookup_concept("Employee") + + assert names(reasoner.ref_scheme(employee)) == ("Employee.nr",) + + +def test_ref_scheme_returns_every_relationship_of_a_composite_identifier(reasoner_for): + """A composite scheme comes back whole, not just its first element. + + Both converter call sites branch on `len(scheme) == 1` and refuse the + composite case with a diagnostic, so a scheme that silently truncated to one + relationship would turn a clear error into a wrong entity key. + """ + ontology, reasoner = reasoner_for("flights") + runway = ontology.ontology.lookup_concept("Runway") + + assert names(reasoner.ref_scheme(runway)) == ("Runway.designator", "Runway.airport") + + +def test_ref_scheme_inherits_from_the_supertype(reasoner_for): + """A subtype with no identifier of its own answers with its parent's. + + `shallow=True` is the way to ask for only what the concept itself declares, + and is the difference between "this concept has no identifier" and "this + concept declares no identifier" — which are not the same question. + """ + ontology, reasoner = reasoner_for("retail") + contractor = ontology.ontology.lookup_concept("Contractor") + + assert names(reasoner.ref_scheme(contractor)) == ("Employee.nr",) + assert reasoner.ref_scheme(contractor, shallow=True) is None + + +def test_ref_scheme_is_none_when_nothing_identifies_the_concept(reasoner_for): + """None, not an empty tuple. + + Both callers lean on the falsiness rather than the type — the formula + emitter writes `ref_scheme(...) or ()` — but the converter reports + "has no reference scheme" off the same value, so the empty case has to stay + falsy whichever way it is spelled. + """ + ontology, reasoner = reasoner_for("retail") + builtin = ontology.ontology.lookup_concept("String") + + assert reasoner.ref_scheme(builtin) is None + + +# --------------------------------------------------------------------------- +# in_subtype_closure +# --------------------------------------------------------------------------- + +def linear_hierarchy() -> dict[str, Concept]: + """C extends B extends A — two hops, so the answer is transitive, not direct.""" + a = Concept(name="A", type=ConceptType.ENTITY_TYPE) + b = Concept(name="B", type=ConceptType.ENTITY_TYPE, extends=[a]) + c = Concept(name="C", type=ConceptType.ENTITY_TYPE, extends=[b]) + return {"A": a, "B": b, "C": c} + + +@pytest.mark.parametrize("arrival", [["A", "B", "C"], ["C", "B", "A"]]) +def test_subtype_closure_does_not_depend_on_registration_order(arrival: list[str]): + """Registration order must not change the answer. + + It used to. The closure was indexed as concepts arrived, and building it + that way only sees a parent's own supertypes if the parent was registered + first — so registering C before A lost the transitive C -> A edge and the + predicate answered False. `spec_to_ossie` topologically sorts its concepts, + which hid this; `palantir_to_ossie` breaks cycles and any caller may build + an OntologyComponent by hand, so neither is protected by that accident. + """ + concepts = linear_hierarchy() + reasoner = OntologyReasoner() + for name in arrival: + reasoner.new_concept(concepts[name]) + + assert reasoner.in_subtype_closure(concepts["C"], concepts["A"]) + assert reasoner.in_subtype_closure(concepts["B"], concepts["A"]) + # Strict: a concept is not its own subtype, and the relation has a direction. + assert not reasoner.in_subtype_closure(concepts["A"], concepts["A"]) + assert not reasoner.in_subtype_closure(concepts["A"], concepts["C"]) + + +# --------------------------------------------------------------------------- +# Malformed input: cyclic `extends` +# --------------------------------------------------------------------------- + +def test_a_cycle_in_extends_terminates_instead_of_recursing(): + """Both walks stop on a cycle rather than exhausting the stack. + + `spec_to_ossie` rejects cycles and `palantir_to_ossie` breaks them, so no + converter here can produce one — but `Concept.extend()` is public and this + is a library, so the failure mode for a hand-built model should be an + answer, not a thousand-frame traceback. Both queries treat an `extends` + edge that leads nowhere new as contributing nothing, which is the only + reading a cycle supports. + + Ingestion is covered too: `new_concept` asks `Concept.is_primitive`, which + walks `extends` on the model side and used to recurse away on a cycle + before the reasoner was asked anything at all. + """ + a = Concept(name="A", type=ConceptType.ENTITY_TYPE) + b = Concept(name="B", type=ConceptType.ENTITY_TYPE, extends=[a]) + a.extend(b) # A extends B extends A + unrelated = Concept(name="Unrelated", type=ConceptType.ENTITY_TYPE) + + reasoner = OntologyReasoner() + reasoner.new_concept(a) + reasoner.new_concept(b) + + assert reasoner.in_subtype_closure(a, unrelated) is False + assert OntologyReasoner.ref_scheme(a) is None + # The cycle itself is still a subtype edge, and is reported as one. + assert reasoner.in_subtype_closure(a, b) + assert reasoner.in_subtype_closure(b, a) + + +def test_a_long_extends_chain_does_not_exhaust_the_stack(): + """Depth is bounded by the model, not by Python's recursion limit. + + `in_subtype_closure` walks iteratively for this reason; a chain deeper than + the interpreter's limit is pathological but it should not be the thing that + decides whether the query works. + """ + chain = [Concept(name="C0", type=ConceptType.ENTITY_TYPE)] + for i in range(1, 2000): + chain.append(Concept(name=f"C{i}", type=ConceptType.ENTITY_TYPE, extends=[chain[-1]])) + + reasoner = OntologyReasoner() + assert reasoner.in_subtype_closure(chain[-1], chain[0]) + assert not reasoner.in_subtype_closure(chain[0], chain[-1]) diff --git a/converters/ontology/uv.lock b/converters/ontology/uv.lock index 303af40e..4519ea14 100644 --- a/converters/ontology/uv.lock +++ b/converters/ontology/uv.lock @@ -1,82 +1,1722 @@ version = 1 revision = 3 requires-python = ">=3.11" +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'win32'", + "python_full_version >= '3.14' and sys_platform == 'emscripten'", + "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'win32'", + "python_full_version < '3.12' and sys_platform == 'win32'", + "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'", + "python_full_version < '3.12' and sys_platform == 'emscripten'", + "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version < '3.12' and sys_platform != 'emscripten' and sys_platform != 'win32'", +] + +[[package]] +name = "aiohappyeyeballs" +version = "2.7.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ce/f4/eec0465c2f67b2664688d0240b3212d5196fd89e741df67ddb81f8d35658/aiohappyeyeballs-2.7.1.tar.gz", hash = "sha256:065665c041c42a5938ed220bdcd7230f22527fbec085e1853d2402c8a3615d9d", size = 24757, upload-time = "2026-07-01T17:11:55.501Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/43/1947f06babed6b3f1d7f38b0c767f52df66bfb2bc10b468c4a7de9eceff2/aiohappyeyeballs-2.7.1-py3-none-any.whl", hash = "sha256:9243213661e29250eb41368e5daa826fc017156c3b8a11440826b2e3ed376472", size = 15038, upload-time = "2026-07-01T17:11:54.055Z" }, +] + +[[package]] +name = "aiohttp" +version = "3.14.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohappyeyeballs" }, + { name = "aiosignal" }, + { name = "attrs" }, + { name = "frozenlist" }, + { name = "multidict" }, + { name = "propcache" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, + { name = "yarl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/58/d9/22ce5786ac0c1653ae8b6c23bded02c1686d11f0dbb45b31ce128e0df985/aiohttp-3.14.3.tar.gz", hash = "sha256:9491196535a88924a60afd5b5f434b5b203b6cc616250878dbdb223a8f7844bc", size = 7971213, upload-time = "2026-07-23T01:57:27.037Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f8/5c/b3e4ff8ad43a8afef9602c5e90285936da1beaea8b029016b793891f03c3/aiohttp-3.14.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:e568e14940c09955aa51f4e645b6daa18a581c5dcfcd73744dcc86a856e3ced3", size = 764250, upload-time = "2026-07-23T01:52:48.525Z" }, + { url = "https://files.pythonhosted.org/packages/0e/da/f1b384465e51449d844056b75070461da03a9a23e6c1747003695bf4172a/aiohttp-3.14.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:54cfcdee2770dac994417cbb0ee1f3eb0e7cb6b30c79bf44f2c02ff79ec5124a", size = 516281, upload-time = "2026-07-23T01:52:51.047Z" }, + { url = "https://files.pythonhosted.org/packages/b9/3f/01264f820ee2e3712a827892b1cd6ff80f3300c1fcbffbb45714a915d47a/aiohttp-3.14.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:21c016079415ed3fd676963e9793700a566d85dbbd6bfc564b9b2d209147dcc8", size = 514742, upload-time = "2026-07-23T01:52:53.779Z" }, + { url = "https://files.pythonhosted.org/packages/9e/8d/a71c6f2db52ac1ed142b133f7feddaa6b70539c3f4de24d7e226c95b794c/aiohttp-3.14.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d6088ec9894113802bddb3c09e974929aed2c7b3a8c456219b8aab4481f1a239", size = 1780613, upload-time = "2026-07-23T01:52:56.948Z" }, + { url = "https://files.pythonhosted.org/packages/a5/11/3dd9b3fb3a170f6ec9011b5291d876a6fab4086714c9e158600edf01b4fd/aiohttp-3.14.3-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:16ea7e24c309fb7c0bbd505d149abe4fe4dccfb8db911db7dbec0921bc889a6f", size = 1737688, upload-time = "2026-07-23T01:52:59.294Z" }, + { url = "https://files.pythonhosted.org/packages/6d/3e/834c26918be7d88068822b40e0db30fca50b5f4fe79104aa16a93f1d74e6/aiohttp-3.14.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:56f355e79f71aef2a85c80305cc915f894b170dba76de5fe84f6351939b83c06", size = 1845742, upload-time = "2026-07-23T01:53:01.641Z" }, + { url = "https://files.pythonhosted.org/packages/cc/c9/49ab8572df7d66bc13d11e31f781292badb04180dd87ba98733066c6aed7/aiohttp-3.14.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:18c441d0a8fca6de8d1f546849b9f0ab20d435993e2c5b59562b2fae6be2f929", size = 1928412, upload-time = "2026-07-23T01:53:04.018Z" }, + { url = "https://files.pythonhosted.org/packages/a5/b9/2b8f0c0ce09c87a1daf80fd483431b56b1435d3f62789bc86f572e1245de/aiohttp-3.14.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:53e7b4ce82b54a8bcc71b3b67a5cbd177ca1d7f592cbc92cd38b7349f73482db", size = 1786220, upload-time = "2026-07-23T01:53:06.481Z" }, + { url = "https://files.pythonhosted.org/packages/85/00/9c45f81de11710460edfa1dc81317b6e882703b160926c879a9d20da9fcc/aiohttp-3.14.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f55119f7bf25f49ed210f6096090715da24f2943c62102448915fde3c62877ce", size = 1637231, upload-time = "2026-07-23T01:53:10.258Z" }, + { url = "https://files.pythonhosted.org/packages/19/ce/967d628e910756f3539c6107cb7844a1b69440dcb3029a5ee7871b09ab63/aiohttp-3.14.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9aa6e61fdf20105c4144e755bd586008ff450791d67b1c8146fdc15959c4d51c", size = 1753161, upload-time = "2026-07-23T01:53:13.817Z" }, + { url = "https://files.pythonhosted.org/packages/11/b2/0c3d4114f0aee4f580f5b3b4eb71b24d7a23b834ea506a4dfebe76513f35/aiohttp-3.14.3-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:ccd4893707b3e2a13e39c90d43cf80edf2e4d0457935bcc103bf2346214c3f15", size = 1756356, upload-time = "2026-07-23T01:53:16.211Z" }, + { url = "https://files.pythonhosted.org/packages/63/5d/99e7d91c82f1399d1ae2a854e080bd1493fbc31e5e959dbc4ec33dac3bec/aiohttp-3.14.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:b2466434105a4e03113c36ec775cc2ebe6676b62eae326fa670bb607ef788c1c", size = 1819846, upload-time = "2026-07-23T01:53:18.289Z" }, + { url = "https://files.pythonhosted.org/packages/ad/05/d5e1cb6480eeffd3f901d40a2c5e2d1e7effdc797837da3b490272699f13/aiohttp-3.14.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:ba59d59aba08ac02fc03b0c8983ccd5ee39a199d0552ce9e6d2b4845b34d59ae", size = 1628531, upload-time = "2026-07-23T01:53:23.86Z" }, + { url = "https://files.pythonhosted.org/packages/c9/90/b934682bcaefae18a9e04f3dff5b68522ba810906358ae5029b68110ea3b/aiohttp-3.14.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:ed099d105449c4f9e84f24af203cd131349d4761d8813fa7e02c32e7128cd910", size = 1832712, upload-time = "2026-07-23T01:53:27.551Z" }, + { url = "https://files.pythonhosted.org/packages/21/df/6061679faaf81fac746e7307c7adb71e858071a5d34c27583afefc64f543/aiohttp-3.14.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:152516815ef926786a0b6ae2b8f1fd2e0c71582dee0b435636865316fd4891b7", size = 1775014, upload-time = "2026-07-23T01:53:30.223Z" }, + { url = "https://files.pythonhosted.org/packages/8a/1d/f854878bbc69b88faefe924b619a34a6f59ec05fd387c77690667eaa75eb/aiohttp-3.14.3-cp311-cp311-win32.whl", hash = "sha256:a4af35c443e0b1a1bd6a8af3f3485d7fda15c142751a00f3ff8090f0b93346fa", size = 456006, upload-time = "2026-07-23T01:53:34.97Z" }, + { url = "https://files.pythonhosted.org/packages/73/0c/2af9d1674baccd1dbd47282a93d660a22e57ef6167c856deb24b4214fbab/aiohttp-3.14.3-cp311-cp311-win_amd64.whl", hash = "sha256:e1e74298bab6ee0d6e749ed4fd1901c7e604bdda32c03d787a2cc71c46d0433d", size = 481069, upload-time = "2026-07-23T01:53:39.673Z" }, + { url = "https://files.pythonhosted.org/packages/8e/76/88401ff3fc95e85c5fc38d588f36f55e61ecb64343b2bc8d69326f453cc0/aiohttp-3.14.3-cp311-cp311-win_arm64.whl", hash = "sha256:03cd2bde3d7f085b64e549c985f4bb928cad7e8ecf5323bfca320db548d81b39", size = 453021, upload-time = "2026-07-23T01:53:43.749Z" }, + { url = "https://files.pythonhosted.org/packages/18/d4/eb96299230e20acf2efae207cb8d69051f1f68e357e5ea5e479bf6fb097a/aiohttp-3.14.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:39aded8c7f3b935b54aab1d8d73c70ec0ee2d3ec3b943e0e86611bc150ba47f5", size = 754690, upload-time = "2026-07-23T01:53:47.332Z" }, + { url = "https://files.pythonhosted.org/packages/88/11/e7a70a209eb9a067c0d3212b518a0134e3484f5178c7533878b6b514d469/aiohttp-3.14.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5bcb6ff3fdab1258a192679ff1a05d44f59626430aa05cd1a9d2447423599228", size = 509484, upload-time = "2026-07-23T01:53:51.159Z" }, + { url = "https://files.pythonhosted.org/packages/30/07/4bbc222cc8dbe31d4c3e8a5baad2286e4d42026ac0c570027b89afce6344/aiohttp-3.14.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:617105e2c3018ee38d0c8ce5ee3c84f621a6d8b9f723202aacaff28449ca91ee", size = 511949, upload-time = "2026-07-23T01:53:55.083Z" }, + { url = "https://files.pythonhosted.org/packages/54/b9/42e74c46b7b7c794b995bbc1f573fb48950c38b19d8600c62a6804ee2d67/aiohttp-3.14.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f631fe87a6f30df5fbe6d79640b25e4cffb38c31c7fb6f10871517b84b0f8c1a", size = 1765282, upload-time = "2026-07-23T01:53:59.662Z" }, + { url = "https://files.pythonhosted.org/packages/6b/ed/62bc4d74363ad346d518e0720363a949f63e2e23439a79eb5813d4d29bb3/aiohttp-3.14.3-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a94dbaae5ae27bd849c93570669bff91e0510f33a80805738e3de72a7be0447b", size = 1741511, upload-time = "2026-07-23T01:54:04.063Z" }, + { url = "https://files.pythonhosted.org/packages/d0/9f/181e8a8bc79e47d13c7fc4540bd7a3b729d9505609c61f392a8dd2fbfe55/aiohttp-3.14.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8f2f1c4c032c7cedd7d8da6f54c97b70266c6570c3108d3fdffee7188bb70529", size = 1810680, upload-time = "2026-07-23T01:54:09.882Z" }, + { url = "https://files.pythonhosted.org/packages/5c/9a/dec94d6ad694552fe3424e3f1928d7a606a5d9d9433a04e7ecdd9d38ae7f/aiohttp-3.14.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ea05e1f97ceea523942d9b2a7d7c0359d781d683d6b043f5943a602b14da4787", size = 1905646, upload-time = "2026-07-23T01:54:13.475Z" }, + { url = "https://files.pythonhosted.org/packages/52/b7/7cd31f29d6055bd711ae6e669367fba6f5ae9de463910a793e30556a8db7/aiohttp-3.14.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:543906c127fb1d929b95076db19b83fa2d46751006ff1e23b093aa5ac4d8db42", size = 1792122, upload-time = "2026-07-23T01:54:15.752Z" }, + { url = "https://files.pythonhosted.org/packages/66/73/10b1ef93afa61f4963c746257b70ced619cf31a4798671de5fdb2608501d/aiohttp-3.14.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0a5ff2dfbb9ce645fa5b8ef3e02c6c0b9cc3f6030ff863d0c51fffc50cb5541b", size = 1591127, upload-time = "2026-07-23T01:54:19.489Z" }, + { url = "https://files.pythonhosted.org/packages/49/ed/3b203fa6de1b338c14acdc06bf6ca9b043b7944f005966958c2ced932cde/aiohttp-3.14.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:041badb8f84396357c4d3ad26de6afd7a32b112f43d3c63045c0c8278cfd2043", size = 1725210, upload-time = "2026-07-23T01:54:24.129Z" }, + { url = "https://files.pythonhosted.org/packages/28/b7/1c2aab8c706436dcc28598452488ac9cd7c409da815237c28c27d58993e6/aiohttp-3.14.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:530125ee1163c4219af35dc3aa1206e541e7b31b6efc1a3f93b70a136f65d427", size = 1764848, upload-time = "2026-07-23T01:54:27.973Z" }, + { url = "https://files.pythonhosted.org/packages/54/50/94c28f08b131c4bf10984ea2c7a536c9920608bb2d6e7f95642c30cc87b7/aiohttp-3.14.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c8653fd547c93a61aadc612007790f5555cdd18946fa48cf45e26d8ea4ea473d", size = 1777102, upload-time = "2026-07-23T01:54:31.775Z" }, + { url = "https://files.pythonhosted.org/packages/13/d4/e7d09ba7d345fb2d74440fd2fa033c5e079fac05552927705986f41a364f/aiohttp-3.14.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:89176250f686cb9853c0fb7ead90e639e915b84a6f43eedc2a4e7ec21f1037f0", size = 1580205, upload-time = "2026-07-23T01:54:34.518Z" }, + { url = "https://files.pythonhosted.org/packages/a3/84/072a91d68e1e1eb587985b54baab94221277f877e8ef274fc213a0ceae28/aiohttp-3.14.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3a26434dafe408229ff3403458ca58de24fb51936504decac49ce6755f77e59d", size = 1797219, upload-time = "2026-07-23T01:54:36.995Z" }, + { url = "https://files.pythonhosted.org/packages/e0/eb/aad34e897e668424d6e995da5dff8a4a09af93363d3392488772957a63aa/aiohttp-3.14.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d1558173930a5a8d3069cee5c92fc91c87c4dbcb099debbb3622053717145a19", size = 1768629, upload-time = "2026-07-23T01:54:40.103Z" }, + { url = "https://files.pythonhosted.org/packages/b6/2b/6bb88ddba0fecd9122aa3ebcad25996cf6c083a4a7040dbb3a4f97972af6/aiohttp-3.14.3-cp312-cp312-win32.whl", hash = "sha256:16100ad3ab8d649fdfbee87602d9d2dcdca9df0b9eda8a1b5fdc0d41f96da559", size = 451481, upload-time = "2026-07-23T01:54:42.547Z" }, + { url = "https://files.pythonhosted.org/packages/76/9b/f2f8f108da17ecef2cc3efc424e8b7ad3782b1a8360f7b8eae8ced84f6ea/aiohttp-3.14.3-cp312-cp312-win_amd64.whl", hash = "sha256:33a2d7c28d33797a2e99923dffa63f83d908a19b6bf26cfe80fa790aa5e1a75a", size = 476845, upload-time = "2026-07-23T01:54:44.853Z" }, + { url = "https://files.pythonhosted.org/packages/3e/44/28dac80a8941b604f4da10ce21097614ca1bf905ce93dca28d8d7de9c1e7/aiohttp-3.14.3-cp312-cp312-win_arm64.whl", hash = "sha256:362a3fd481769cac1a824514bcd86fda51c65e8fe6e051099e008fddde6db17c", size = 448050, upload-time = "2026-07-23T01:54:47.087Z" }, + { url = "https://files.pythonhosted.org/packages/57/be/5afd201cc0ab139029aadb75392efe85a293403d9dd3a3226161c21ce00c/aiohttp-3.14.3-cp313-cp313-android_21_arm64_v8a.whl", hash = "sha256:2e9878ae68e4a5f1c0abe4dd497dbc3d51946f5837b56759e2a02e78fa90ef86", size = 506269, upload-time = "2026-07-23T01:54:49.075Z" }, + { url = "https://files.pythonhosted.org/packages/22/09/dec8189d62b45ade009f6792a2264b942a90cb88aeaf181239933cd72c3c/aiohttp-3.14.3-cp313-cp313-android_21_x86_64.whl", hash = "sha256:f3d2669fe7dec7fc359ecdb5984b29b50d85d5d00f8c1cb61de4f4a24ee42627", size = 515166, upload-time = "2026-07-23T01:54:51.894Z" }, + { url = "https://files.pythonhosted.org/packages/28/24/2854869d29ed8a8b19d74f9ec6629515f7e04d02dd329d9d179201e58e47/aiohttp-3.14.3-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:cc7cb243a68167172f48c1fd43cee91ec4b1d40cefd190edd43369d1a6bc9c82", size = 486263, upload-time = "2026-07-23T01:54:54.223Z" }, + { url = "https://files.pythonhosted.org/packages/d4/dd/57187c8be2a35aea65eaee3bd2c3dcbbcf0204f5106c89637e3610380cd1/aiohttp-3.14.3-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:78253b573e6ffab5028924fc98bc281aae05445969982a10864bc360dea2016c", size = 492299, upload-time = "2026-07-23T01:54:56.236Z" }, + { url = "https://files.pythonhosted.org/packages/b9/11/06ae6ed8f0d414edf4068861e233d8fe23ee699bfd4b3ceb8663db948a62/aiohttp-3.14.3-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:7041d52c3a7fa20c9e8c182b534704abb19502c8bdcbde7ab23bfda6f642394f", size = 502235, upload-time = "2026-07-23T01:54:58.377Z" }, + { url = "https://files.pythonhosted.org/packages/7e/a3/559639c34a345d2cf7c52dff6838119f2eaf29eb508227b5b83f573af813/aiohttp-3.14.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ac74facc01463f138b0da5580329cfcc82818dea5656e83ddcd11268fc12ff80", size = 750883, upload-time = "2026-07-23T01:55:00.65Z" }, + { url = "https://files.pythonhosted.org/packages/91/cd/41e131f13afd1e7b0172a9d9eda085ef90eb8439f41f0d279db81ed3ae60/aiohttp-3.14.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d6218d92e450824e9b4881f44e8c09f1853b490f9a64130801024a4793b1b3b0", size = 508473, upload-time = "2026-07-23T01:55:02.945Z" }, + { url = "https://files.pythonhosted.org/packages/bc/6b/e7f13410d391c6e55b4c007a8de024355389d7d459e3d64c42b2d33617e5/aiohttp-3.14.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:11fb37ef075669eee52ab1928fbf6e1741fada40409fa309ebde9607a962aebf", size = 509190, upload-time = "2026-07-23T01:55:05.173Z" }, + { url = "https://files.pythonhosted.org/packages/97/21/6464573e53d69672cc1eada3e5c5cb2d2efa82701e8305a0f2047a576967/aiohttp-3.14.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:55bdcc472aafe2de4a253045cc128007a64f1e0264fb675791e132ea5edaa3bd", size = 1761478, upload-time = "2026-07-23T01:55:07.383Z" }, + { url = "https://files.pythonhosted.org/packages/1a/81/d217043a4c17fbce360905e3b2bdd20139ebc9a2de836d035d179c4da006/aiohttp-3.14.3-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c39846c3aad97a8530c89d7a3869a8f8e9e3762c6ac0504481e5c80948f7e807", size = 1735092, upload-time = "2026-07-23T01:55:09.803Z" }, + { url = "https://files.pythonhosted.org/packages/a1/66/e13a02d0eeb1a9a502402a977abb4e4abff9fe4051c26f80558c57a7c975/aiohttp-3.14.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5895ef58c4620afe02fa16044f023dc4dafec08158f9d08874a46a7dbc0341b8", size = 1800546, upload-time = "2026-07-23T01:55:12.012Z" }, + { url = "https://files.pythonhosted.org/packages/26/5e/57d42fca1d18cb5acc1cad945d017fabc5d6ae71d8a08ad66be8dc3ee544/aiohttp-3.14.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fa9467a8113aa69d3d7c55a70ef0b7c636010a40993f3df9d9d0d73b3eb7ef24", size = 1895250, upload-time = "2026-07-23T01:55:14.357Z" }, + { url = "https://files.pythonhosted.org/packages/ca/1c/7da8d08e74d56f00070822f9638ff3f1c563f8ad87d1efa996c87bfc8644/aiohttp-3.14.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d7d2deec16eeedf55f2c7cf75b521ea3856a5177e123844f8fd0f114ce252cb5", size = 1789289, upload-time = "2026-07-23T01:55:16.668Z" }, + { url = "https://files.pythonhosted.org/packages/cd/0f/cf16bcf56896981c1a0319f5d5db9337994b5165730c48a8fa07e9b34be6/aiohttp-3.14.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:dd54d0e8717de95939766febac482ac0474d8ac3b048115f9f2b1d23a16e7db4", size = 1586706, upload-time = "2026-07-23T01:55:18.913Z" }, + { url = "https://files.pythonhosted.org/packages/fe/6f/76eac12a7f2480e1e304f842efdb07db33256b0d9165b866b6ef0806c202/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:df82f3787c940c94986b34222d59c9e38843fba85139f36e85255a82ad5355a9", size = 1724652, upload-time = "2026-07-23T01:55:21.296Z" }, + { url = "https://files.pythonhosted.org/packages/39/b6/19c8c592baeeb94b75f966547d40c02ac7590902306ec5863d5c027cf506/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:42a67efc36300d052fb4508a53e8b6901b9284b599ae63945c377569c5fcc1e1", size = 1756239, upload-time = "2026-07-23T01:55:23.705Z" }, + { url = "https://files.pythonhosted.org/packages/dc/c9/4e9383150296f97f873b680c4de8fb2cd88608fb9f48c79edcb111611abc/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7a75aa63cbf9b21cfaf60dc2657e19df2c2867d91707d653fee171ffeedd1371", size = 1769161, upload-time = "2026-07-23T01:55:26.082Z" }, + { url = "https://files.pythonhosted.org/packages/aa/1e/147bdc6cc5de5f3ab011be8bf5d6e786633249f22c20bae06f85e45f5387/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:e92eb8acc45eb6a9f4935071a77edf5b85cc6f8dfad5cd99e97653c26593cdde", size = 1578759, upload-time = "2026-07-23T01:55:28.846Z" }, + { url = "https://files.pythonhosted.org/packages/fd/31/78388a9d6040ece2e11df62ea229a822cf5e52d238374b220ae9975b2623/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b014a6ed7cf912e787149fdc529166d3ceabac23f26efeea3158c9aba2354e7e", size = 1792025, upload-time = "2026-07-23T01:55:31.457Z" }, + { url = "https://files.pythonhosted.org/packages/03/51/a3d29fdf2c25d796746af8ad6fe56a45d6256c38b0a8a2ed752e1160b3a2/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:3d4f72af88ac2474bb5bca640030320e3d38a0163a1d7533500e87be458eef71", size = 1768477, upload-time = "2026-07-23T01:55:33.87Z" }, + { url = "https://files.pythonhosted.org/packages/29/a6/442e18b5afeade534d877a2dc3c3e392aff8d49787890b0cf84790410267/aiohttp-3.14.3-cp313-cp313-win32.whl", hash = "sha256:5f08ec777f35ee70720233b8b9811d3bb5d728137f30ac91b7457709c3261ac0", size = 451069, upload-time = "2026-07-23T01:55:36.121Z" }, + { url = "https://files.pythonhosted.org/packages/9d/69/3d876ac02659f271cf7f6769f14a8e3de5b6e888ed8b5a7e998086a4cec8/aiohttp-3.14.3-cp313-cp313-win_amd64.whl", hash = "sha256:dff9461ec275f22135650d5ba4b4931a11f3958df7dfbb8db630000d4dee0883", size = 476518, upload-time = "2026-07-23T01:55:38.303Z" }, + { url = "https://files.pythonhosted.org/packages/b2/0e/50d6e6471cd31edce8b282bdec59375a3a69124d8a989a0b1313355cae52/aiohttp-3.14.3-cp313-cp313-win_arm64.whl", hash = "sha256:ddcac3c6b382e81f1dd0499199d4136b877beb4cb5ef770bbbfba56c4b8f55d2", size = 447676, upload-time = "2026-07-23T01:55:40.451Z" }, + { url = "https://files.pythonhosted.org/packages/c8/20/887fdcf832326571b370ffc347b3e70abe101096f3720126aac161b1d872/aiohttp-3.14.3-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:49f7325beb0f85ef4aef5f48f490269575f83e6e2acad00a1d80b807eb027062", size = 509067, upload-time = "2026-07-23T01:55:42.618Z" }, + { url = "https://files.pythonhosted.org/packages/ad/a3/92cec936f78cc4bf0fa5554ebe593b73459d94e3c62303e1902a4cccb6f7/aiohttp-3.14.3-cp314-cp314-android_24_x86_64.whl", hash = "sha256:e3be98a7c30b8c25d573dafba7171d66dfb05ee6a9070fc46535464ff97700a6", size = 514774, upload-time = "2026-07-23T01:55:44.937Z" }, + { url = "https://files.pythonhosted.org/packages/29/ba/2a0c38df3fc557620b6a5acd98364af050053b6285b4dc7ee74100c63c18/aiohttp-3.14.3-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:614c61d478b83953e261d02bb2df750f17227cd33ef8002945bf5aebbde21919", size = 488134, upload-time = "2026-07-23T01:55:47.135Z" }, + { url = "https://files.pythonhosted.org/packages/48/d6/d51b7d4bf309af3693940d8ffd2b9ed0b682434ef85959b7c9c137f60cf8/aiohttp-3.14.3-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:1caa7b0d05f3e3a36f87788c59e970a7ee1cefcfcbb924a9f138c4a6551c9cb7", size = 494201, upload-time = "2026-07-23T01:55:49.451Z" }, + { url = "https://files.pythonhosted.org/packages/3f/5a/8f624384e5f1efabb5229b94157eb966b021e97bdb188c62860c2ae243c2/aiohttp-3.14.3-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:dfa68deb2a443bdaa3ea5297b0699c1464f08aef3812b486d1348eee61b07dc0", size = 502766, upload-time = "2026-07-23T01:55:51.656Z" }, + { url = "https://files.pythonhosted.org/packages/a6/26/4ff0164370deec18fb19254ee4ab10b7a73304ac0c860b13f5f84663759b/aiohttp-3.14.3-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:e72ee89e28d907a18f46959b4eb0bb06701cc7f8cf4366e00029e2ccfaaf5924", size = 756557, upload-time = "2026-07-23T01:55:53.964Z" }, + { url = "https://files.pythonhosted.org/packages/97/a3/7056b86dc0d9ec709ea9777eae3b0161428f943372f8b98c01c11593b682/aiohttp-3.14.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:ad4c8b7488d745d2ca4838ebd8ae5ba9b56341d30b1da43640e4ce87f9f49646", size = 510168, upload-time = "2026-07-23T01:55:56.22Z" }, + { url = "https://files.pythonhosted.org/packages/85/ed/0357a015892fd68058bf2d39d3fd1958e459b997a7db30aaa6aaa434ae96/aiohttp-3.14.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:db332af25642007330fca8be5c4d194caf2bea7a7fc84415aff3497af5dfee6b", size = 512957, upload-time = "2026-07-23T01:55:58.437Z" }, + { url = "https://files.pythonhosted.org/packages/47/d1/8aba53f15ccb2238405f5e9d30e2a8ca44f93878c26e7165ade00d374b1c/aiohttp-3.14.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:25bd2708db6bdf6a6630dd37bdcdfcb47c4434d22ac69c64665b802910140b30", size = 1750149, upload-time = "2026-07-23T01:56:00.856Z" }, + { url = "https://files.pythonhosted.org/packages/49/bd/40c3fee327529284375c6701cbb0fa4600cc2e8432af1378f897e2ef7d3a/aiohttp-3.14.3-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:cef89a58e628c4efcac3275c2d68083f82426dcdc89c1492a6f654f9f7ea6ab9", size = 1707685, upload-time = "2026-07-23T01:56:03.371Z" }, + { url = "https://files.pythonhosted.org/packages/2a/a3/ca0cc6724cca8114b05694abd916060758c79894c3aa5b012cdadc1bc28e/aiohttp-3.14.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c23ec8ee9d5ab2f5421f9c7fffce208435607af27fd46d4a44e031954352838f", size = 1803911, upload-time = "2026-07-23T01:56:05.817Z" }, + { url = "https://files.pythonhosted.org/packages/95/b5/85b099c299c3ffd38ad9b3e43694c8a346934e4a30c88c4fd5a841234f77/aiohttp-3.14.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e2667f0bbe7eb6c74eae5e9691441ad186e5845ca3cff63230fc09c4e7514f5d", size = 1876929, upload-time = "2026-07-23T01:56:08.413Z" }, + { url = "https://files.pythonhosted.org/packages/d5/b7/1da684a04175473fa4cddbf9a2f572e79514c3fd27a74597f43057d4f3da/aiohttp-3.14.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18cb43369747b2ae007bd2655fb8e63a099c2ff1d207962943636dac989b3147", size = 1761112, upload-time = "2026-07-23T01:56:10.918Z" }, + { url = "https://files.pythonhosted.org/packages/d1/16/bc4b55e3e5cb175fd69c53c90d60d2f47797cb343da5106e23863dc4dba4/aiohttp-3.14.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d77640cc618c1d99fc4f8589c0f24a730adfa54eb1e57ef7bf0c8dfb78da898c", size = 1583500, upload-time = "2026-07-23T01:56:13.613Z" }, + { url = "https://files.pythonhosted.org/packages/2a/e8/13a9d957a1ee40837f46aa30f0f4c657e673ad86a2e6362a9f9be20d26d9/aiohttp-3.14.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:53e5179d8abb5710f8e83ba207c41c8d1261fcffd4616500e15ca2b7a33be10a", size = 1713940, upload-time = "2026-07-23T01:56:15.969Z" }, + { url = "https://files.pythonhosted.org/packages/38/05/d33c680c1bcf1c7e130f9cbfc1fc02fe8bb0c4af2a94a53dd5fb56131e5c/aiohttp-3.14.3-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:cd817772b2fcf2b8c0905795318485f9ec16eae60b29feb7f4c77085311637f0", size = 1724413, upload-time = "2026-07-23T01:56:18.591Z" }, + { url = "https://files.pythonhosted.org/packages/85/1d/af798d306f7a74b6a632dbcabcf62a4c91391b7582d2a8c6d7712e2cc54e/aiohttp-3.14.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:4e3ac92d90e92773b2362d506068e9a948192bd553e743c5b2429e28527c8661", size = 1770748, upload-time = "2026-07-23T01:56:21.074Z" }, + { url = "https://files.pythonhosted.org/packages/a8/92/ad720d472556a995049206867765e9410969684f86ee09423ff9969044c1/aiohttp-3.14.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:3f42e9b78301f11c8f861746175d8b9c1ccef713fcad9eab396e2f6db8ed4a22", size = 1577564, upload-time = "2026-07-23T01:56:23.475Z" }, + { url = "https://files.pythonhosted.org/packages/60/ad/0ed7586cbef7a884e23a752fa2bb987a122e6a5dd50dab109258d0a95193/aiohttp-3.14.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:9d9edccfe496b476db5f398d97b865e9a6752bcf8aec4eef8390ce20fb64bb41", size = 1782080, upload-time = "2026-07-23T01:56:25.994Z" }, + { url = "https://files.pythonhosted.org/packages/97/ea/dbaed0d73e8a69aad653b045dab451c67c2454bb731a37b45a86593e9422/aiohttp-3.14.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1c5ec8fb1bcc31a8466f74aaf26c345d5c386fa4bd08a3f0eb9c7a4a3fe8b5bf", size = 1745813, upload-time = "2026-07-23T01:56:28.604Z" }, + { url = "https://files.pythonhosted.org/packages/81/1b/6893d4bc57e434fc93a6c9217c637d967a0b651d989f6e3265179375754a/aiohttp-3.14.3-cp314-cp314-win32.whl", hash = "sha256:38901a84da3ce22249f6e860bf8f90d141bcab7da090cc398f8bb58c0e44b7da", size = 455872, upload-time = "2026-07-23T01:56:31.031Z" }, + { url = "https://files.pythonhosted.org/packages/f5/8b/c7baa1ba1eda4db6989baefe5de6d99834921b84ebd7918624febcb9f290/aiohttp-3.14.3-cp314-cp314-win_amd64.whl", hash = "sha256:8b3b60de05f3dcb6f6a00f818bb2ec781cee4de0645f59ccaf99b1d1823b6100", size = 481030, upload-time = "2026-07-23T01:56:33.365Z" }, + { url = "https://files.pythonhosted.org/packages/22/8c/c29d067df825a2df88ca432db848aa2fe8199598359cc06c12b09320cac9/aiohttp-3.14.3-cp314-cp314-win_arm64.whl", hash = "sha256:1576145bdceeb92382d899751e12743a3a5b8e460a841e3e50543859e54864dc", size = 453669, upload-time = "2026-07-23T01:56:35.731Z" }, + { url = "https://files.pythonhosted.org/packages/6a/a4/9c033beb355d39b6147980597ec9645e4729243f686ee4dc73945de72030/aiohttp-3.14.3-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:8800c996b01c2772a783e3e46f3e1abd5823029adca0df54231960de9bfefa5b", size = 791403, upload-time = "2026-07-23T01:56:37.972Z" }, + { url = "https://files.pythonhosted.org/packages/80/ca/87c32a0a7704583cfc49660bd817889bae5b830bf53b5dcb4e92145ac2da/aiohttp-3.14.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:ebe8e504f058fe91223351cecd2d9d6946c9d241bb0250d898ffbdf584cc72b0", size = 526413, upload-time = "2026-07-23T01:56:40.523Z" }, + { url = "https://files.pythonhosted.org/packages/9e/d8/8ec0e471248c500acdce2be3f46db8fb62b5eb60efef072529cc85ee1d26/aiohttp-3.14.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:30402d03a7c0ff52bce290b57e564e9079fd9d0cb545c8aba73f86a103162d2e", size = 532135, upload-time = "2026-07-23T01:56:42.876Z" }, + { url = "https://files.pythonhosted.org/packages/fe/45/f8919fd936e8b79fcd9bda7b6d8e62613462a713f4f17987fd7c34399142/aiohttp-3.14.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9fc7b5bfec6573f3ae844f457fdde5adeb713f8b8e4a81ad64fc207b49383716", size = 1922742, upload-time = "2026-07-23T01:56:45.528Z" }, + { url = "https://files.pythonhosted.org/packages/f6/ec/9ca76b28a27525b0cc53e20842e0228b022f301ce1f436b7d814b4aaf2df/aiohttp-3.14.3-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8a5fd34f7f7410d1730d5c2ba873cacb2eed3fede366feb268a70ba22581ed8f", size = 1787371, upload-time = "2026-07-23T01:56:48.045Z" }, + { url = "https://files.pythonhosted.org/packages/b1/04/6acdbf17315f7b55f1937e3387acb89a3cddeb4995689553d064af8e92ab/aiohttp-3.14.3-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:270d3dace9ca2f10f0da5d8ebe519b7a310fc6112ed916e32df5866df0888553", size = 1912623, upload-time = "2026-07-23T01:56:50.605Z" }, + { url = "https://files.pythonhosted.org/packages/86/e6/438b0c79ca6f45eb9fd9817dd4c01a91919a38c0de5ee9e05e2b4dc0ece7/aiohttp-3.14.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3ae5b3a59436d089b5395d910121a390feed4d00578eb95a0fd1a329fe963100", size = 2005515, upload-time = "2026-07-23T01:56:53.153Z" }, + { url = "https://files.pythonhosted.org/packages/bb/6b/62cbd6577758699525f5c712d1ddef57d9875fbab0ae8d5f5a202fd598f8/aiohttp-3.14.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2498f0fe69ead802f9675beca44a7c21c62fdaa4ec5145ea1c3ad6edbee29f85", size = 1879906, upload-time = "2026-07-23T01:56:55.818Z" }, + { url = "https://files.pythonhosted.org/packages/00/95/18bcbf830a21dc3aae24d8f6b6feaf3db1d2090242d00a7868db2ffb0b67/aiohttp-3.14.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a0dc483c00da8b673abbb367eb6f8d8f4bcec30eb58529ea13cb42e7fd2dfa33", size = 1675849, upload-time = "2026-07-23T01:56:58.861Z" }, + { url = "https://files.pythonhosted.org/packages/a9/19/47f4968659c5e23606c3790c80fc624e691c153d036148449ee84d31b287/aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c7d3a97c678d34fc5b59da671ee9cd630096ddc643e7b5a30d54a2a6f3574d3f", size = 1843496, upload-time = "2026-07-23T01:57:01.591Z" }, + { url = "https://files.pythonhosted.org/packages/64/af/38c33c4dd82fddcb4e56c4653b6f1072a8edbc6b7fa15809f14932c41e2d/aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:f8fb78a83c9e5f741ca3a68cfb455c1f5bb83b4e7249a3848b3cd78d0a8563b0", size = 1827746, upload-time = "2026-07-23T01:57:05.131Z" }, + { url = "https://files.pythonhosted.org/packages/a1/9d/0537cda4885ac8f5b7053d164dd06312f4c483a4edcb8ee5b8aaf2a989bf/aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:74ab5b6a9fb13e873e5a90946588baecaf488745e1db1a4a5c433f971f035098", size = 1853810, upload-time = "2026-07-23T01:57:08.043Z" }, + { url = "https://files.pythonhosted.org/packages/19/fe/26f9c5e6458385aa86497836b0dea6fb2f027827d63f37c7856cce9286ee/aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:bd52f811e65f6fb634b1047159657c98f52b407f8efec907bcfc09da9a4c0a25", size = 1668895, upload-time = "2026-07-23T01:57:10.837Z" }, + { url = "https://files.pythonhosted.org/packages/ec/4c/618b1db9b9ba079b8875d2cdf78e7c4a3bf72903bd5850fee7dd9544600a/aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:f0f177d1b195b9e06376cfd7d308d8a1b920909a609d03ac82a8c73bbb16d3b9", size = 1883833, upload-time = "2026-07-23T01:57:13.672Z" }, + { url = "https://files.pythonhosted.org/packages/94/c6/bd959bd1e4771f9fd944e9e436224c48c77b018b73b519b5aad346335bcc/aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:498c6c623134f8e09a3c4e60bcd607a0b4590dd7dbf08dd40851b27cbb520ccb", size = 1844251, upload-time = "2026-07-23T01:57:16.593Z" }, + { url = "https://files.pythonhosted.org/packages/5e/19/08d41839658bdd44a0ed2480f3891705ecb487ce28c0dde62c9040c997e0/aiohttp-3.14.3-cp314-cp314t-win32.whl", hash = "sha256:b304db572b4368edd8dda8a2274f73156fe15558fca4a917cb8a09fc47af5963", size = 474180, upload-time = "2026-07-23T01:57:19.306Z" }, + { url = "https://files.pythonhosted.org/packages/99/5d/3cd6ef0a2b2851f7ab913b5b079334781bd50ff56a323e4454063377a080/aiohttp-3.14.3-cp314-cp314t-win_amd64.whl", hash = "sha256:b20032766aedf6261c7a566585a40867d092ac03a0d81592d5370ef9b054f99b", size = 500528, upload-time = "2026-07-23T01:57:21.762Z" }, + { url = "https://files.pythonhosted.org/packages/a4/37/cfd1ed540a4d318da025590d96b728e63713c09e9377950fc655dadeb856/aiohttp-3.14.3-cp314-cp314t-win_arm64.whl", hash = "sha256:2e1161602f45a54de2ce0905243a95f58cb42dcd378402f3697f5e0b21e9d2e7", size = 469280, upload-time = "2026-07-23T01:57:24.241Z" }, +] + +[[package]] +name = "aiosignal" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "frozenlist" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/61/62/06741b579156360248d1ec624842ad0edf697050bbaf7c3e46394e106ad1/aiosignal-1.4.0.tar.gz", hash = "sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7", size = 25007, upload-time = "2025-07-03T22:54:43.528Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e", size = 7490, upload-time = "2025-07-03T22:54:42.156Z" }, +] + +[[package]] +name = "annotated-doc" +version = "0.0.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/8e/38aa427ed5402449e226975b649c5dc73ccadfefeb95e6aecb8f8ea4b6b6/annotated_doc-0.0.5.tar.gz", hash = "sha256:c7e58ce09192557605d8bbd92836d7e1d520ac9580096042c0bfd197efacf1bb", size = 10758, upload-time = "2026-07-28T13:50:58.129Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3e/30/e900b21425a860e195f32e37657aa1f7c7f2b1bfb26f03ca209b90933c06/annotated_doc-0.0.5-py3-none-any.whl", hash = "sha256:117bac03a25ede5df5440e855b32d556049ca169ead221505badf432fed4b101", size = 5302, upload-time = "2026-07-28T13:50:57.239Z" }, +] + +[[package]] +name = "annotated-types" +version = "0.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5f/56/a8120250d128bed162cd73c76d45f6ef9991f3e068f62a8ee060afa3104a/annotated_types-0.8.0.tar.gz", hash = "sha256:13b2beaad985e05e2d6407ee4c4f35590b11f8d693a258a561055cac8f64cab7", size = 15893, upload-time = "2026-07-23T20:16:13.995Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/99/91/8acff4f5e50511b911bbccb72b8628a49c68ce14148cd9f6431094859a90/annotated_types-0.8.0-py3-none-any.whl", hash = "sha256:f072f4d804ea359e4eaf198b1af7a8b0943881a87f31bb764f8bf219bb9419e0", size = 13427, upload-time = "2026-07-23T20:16:12.938Z" }, +] + +[[package]] +name = "anyio" +version = "4.15.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "typing-extensions", marker = "python_full_version < '3.15'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a9/d2/f4d173e22df740bc37b1db102b386ba719b66e95b0f0d751f556b387e6d2/anyio-4.15.1.tar.gz", hash = "sha256:9f28306018cbd6d329e64a36d58256edff76dd996fe423bc957326e578b82a94", size = 276966, upload-time = "2026-09-05T10:42:39.44Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/b8/4bd346e22b28902df4d651910f5242c28d84e4a5c2435ca5c3f797ed7e2e/anyio-4.15.1-py3-none-any.whl", hash = "sha256:6152fdbbf9a77fdec97731721bebf7c4c44f7c29b424b0065826173efc7ed101", size = 132079, upload-time = "2026-09-05T10:42:37.923Z" }, +] + +[[package]] +name = "apache-ossie-ontology" +version = "0.1.0" +source = { editable = "." } +dependencies = [ + { name = "ply" }, + { name = "pydantic" }, + { name = "pyyaml" }, +] + +[package.optional-dependencies] +relationalai = [ + { name = "more-itertools" }, + { name = "pandas" }, + { name = "relationalai" }, +] + +[package.dev-dependencies] +dev = [ + { name = "pytest" }, + { name = "pytest-snapshot" }, +] + +[package.metadata] +requires-dist = [ + { name = "more-itertools", marker = "extra == 'relationalai'" }, + { name = "pandas", marker = "extra == 'relationalai'" }, + { name = "ply" }, + { name = "pydantic" }, + { name = "pyyaml" }, + { name = "relationalai", marker = "extra == 'relationalai'", specifier = "==1.27.1" }, +] +provides-extras = ["relationalai"] + +[package.metadata.requires-dev] +dev = [ + { name = "pytest", specifier = "==9.0.3" }, + { name = "pytest-snapshot" }, +] + +[[package]] +name = "asn1crypto" +version = "1.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/de/cf/d547feed25b5244fcb9392e288ff9fdc3280b10260362fc45d37a798a6ee/asn1crypto-1.5.1.tar.gz", hash = "sha256:13ae38502be632115abf8a24cbe5f4da52e3b5231990aff31123c805306ccb9c", size = 121080, upload-time = "2022-03-15T14:46:52.889Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c9/7f/09065fd9e27da0eda08b4d6897f1c13535066174cc023af248fc2a8d5e5a/asn1crypto-1.5.1-py2.py3-none-any.whl", hash = "sha256:db4e40728b728508912cbb3d44f19ce188f218e9eba635821bb4b68564f8fd67", size = 105045, upload-time = "2022-03-15T14:46:51.055Z" }, +] + +[[package]] +name = "attrs" +version = "26.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9a/8e/82a0fe20a541c03148528be8cac2408564a6c9a0cc7e9171802bc1d26985/attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32", size = 952055, upload-time = "2026-03-19T14:22:25.026Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548, upload-time = "2026-03-19T14:22:23.645Z" }, +] + +[[package]] +name = "backports-tarfile" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/86/72/cd9b395f25e290e633655a100af28cb253e4393396264a98bd5f5951d50f/backports_tarfile-1.2.0.tar.gz", hash = "sha256:d75e02c268746e1b8144c278978b6e98e85de6ad16f8e4b0844a154557eca991", size = 86406, upload-time = "2024-05-28T17:01:54.731Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b9/fa/123043af240e49752f1c4bd24da5053b6bd00cad78c2be53c0d1e8b975bc/backports.tarfile-1.2.0-py3-none-any.whl", hash = "sha256:77e284d754527b01fb1e6fa8a1afe577858ebe4e9dad8919e34c862cb399bc34", size = 30181, upload-time = "2024-05-28T17:01:53.112Z" }, +] + +[[package]] +name = "boto3" +version = "1.43.95" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "botocore" }, + { name = "jmespath" }, + { name = "s3transfer" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/ed/9d4d4e7d4b874c16f7b3a886efe3e45801e0f9ebb60058adf1bb69cb2073/boto3-1.43.95.tar.gz", hash = "sha256:9d71f299111e1f4e8c28f573a1b7c0555fe40d2147fe9bef852a02bd57cbde60", size = 112666, upload-time = "2026-09-15T19:23:49.395Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/66/6e/9c6fb58b1cdddf13b3e9edc94e0c6e08fff456355f6e097684d6117aa8d4/boto3-1.43.95-py3-none-any.whl", hash = "sha256:c906921c4f9ab41e9587af6586f072c8f2be6979e8bde2ec26aa443bb1745019", size = 140026, upload-time = "2026-09-15T19:23:47.563Z" }, +] + +[[package]] +name = "botocore" +version = "1.43.95" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jmespath" }, + { name = "python-dateutil" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/db/2c/0031468b521eefed325a3da2d6576e068aab95837af0c96effea6438851c/botocore-1.43.95.tar.gz", hash = "sha256:779588da32bd48a7bb0c097da4bcb747260e86d2bfc507369dca56f1450e0722", size = 16106887, upload-time = "2026-09-15T19:23:44.23Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/77/7ce7c937b31903f1402e478fdc97f98e727656fcea3cb54e74f1ca55793e/botocore-1.43.95-py3-none-any.whl", hash = "sha256:0fda26d16c7c7bf7082c421390a817b696f98a43d3da7c43dbbb093f76662876", size = 15800616, upload-time = "2026-09-15T19:23:41.228Z" }, +] + +[[package]] +name = "certifi" +version = "2026.7.22" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a3/c2/24167ea9858356b47a87a50d39908bfdb72ceeefe0041586e704e5376b3a/certifi-2026.7.22.tar.gz", hash = "sha256:741e2c3b351ddf169a738da9f2c048608ff7f2c5cc02f1ebc6b118bb090d5d55", size = 138112, upload-time = "2026-07-22T03:35:12.644Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/a7/71ac2cff56fec219ed242bb11b8efb69fcc4bec75db06fb7bfe35de520e6/certifi-2026.7.22-py3-none-any.whl", hash = "sha256:62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775", size = 136983, upload-time = "2026-07-22T03:35:11.276Z" }, +] + +[[package]] +name = "cffi" +version = "2.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser", marker = "implementation_name != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9e/ef/008a1939e372c06329a3fce4279c02f328488f3526744906eeec3da7ad5f/cffi-2.1.1.tar.gz", hash = "sha256:dd31f52ea1086513bb9df30f8fcee9b8918323ae067a3d5b78bc826a000712be", size = 530807, upload-time = "2026-08-03T21:21:18.939Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/70/d2/16d99a0c4948febc0ebd133a13b2f688ff7f8cb04da971e1128872ce0c03/cffi-2.1.1-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:c8d2c9fd1f2d16f780d15127abb050d13d1a76c03a4bd87d7e4980e45e511e12", size = 183838, upload-time = "2026-08-03T21:19:29.637Z" }, + { url = "https://files.pythonhosted.org/packages/cd/95/31b535a9f0220ae9f357de4a08d57ce89cb417653c2fd9f075f50822a388/cffi-2.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:398aff33cee2767e3e781d2554c54bd0dff386bb437581e0d8011fde1a942ec1", size = 184168, upload-time = "2026-08-03T21:19:30.764Z" }, + { url = "https://files.pythonhosted.org/packages/ad/5a/4707a0dc1f203f5dde5a907b0d4e3c25d71120241048bd5bc6f1bb9d4e71/cffi-2.1.1-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:154852545011f779917b11c78db2358d095da62a9a172b78ad0a583ee5adc0d0", size = 211805, upload-time = "2026-08-03T21:19:31.867Z" }, + { url = "https://files.pythonhosted.org/packages/ad/66/c19feabb28485b6e0bbaaafa90837a1ef5d302e90f2178bd33f17a49879b/cffi-2.1.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3311ed60d36f83378794e1009ac6258bafbf81f7888b4caa7b35a521e3f95813", size = 218716, upload-time = "2026-08-03T21:19:32.896Z" }, + { url = "https://files.pythonhosted.org/packages/a7/92/500760486c8baab49a7a8a58ba7fc3355ec3974b454b8a09e528efde9e1d/cffi-2.1.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6e192623c49c94421616a5778fba35cf0d5a8d000650c1967ef4448ee5cdd990", size = 205569, upload-time = "2026-08-03T21:19:34.142Z" }, + { url = "https://files.pythonhosted.org/packages/a5/a7/a67c733254d6e7373f7822f8082d8d6beade791e0cf12a7611f376fa61c7/cffi-2.1.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:a6e721d4b0e45d5b65e87534470e67b18dcd092c83f68fba09f152b9cbc061af", size = 204907, upload-time = "2026-08-03T21:19:35.174Z" }, + { url = "https://files.pythonhosted.org/packages/f7/a4/4399daaf8f7dfee9d7c3327fdb0426ee041cc63edc358b93911ceb2bfc7a/cffi-2.1.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:34e261f78cb6ceaaa36f42f2613f4380d94d9c759a9c73c769ee6e0247364632", size = 217807, upload-time = "2026-08-03T21:19:36.286Z" }, + { url = "https://files.pythonhosted.org/packages/28/f7/dabe6da2466ecbd82dc62e7342dc6b1065dad990c06f00f0ede9ebf2a0ed/cffi-2.1.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7225e4514edb64eb6740324353e0da0711954fd8d7da4576755b1c6e09b697cd", size = 221252, upload-time = "2026-08-03T21:19:37.416Z" }, + { url = "https://files.pythonhosted.org/packages/ce/87/616202d8e51342c07d2534c510111c4cc37201775ce8f60802c9335d1edd/cffi-2.1.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:df913725b79db7bcf03448f36b7bf8815363417d5b58deecf9305e3e30f0f21a", size = 214214, upload-time = "2026-08-03T21:19:38.507Z" }, + { url = "https://files.pythonhosted.org/packages/b4/c6/ab025d75d2c26c19b087c0124e75ee31cb65032f4fe345d356d8c507ab97/cffi-2.1.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f5cfbc5fe74540d335175b656c725d74d90e3730c626d92575eea35029d9afaa", size = 219408, upload-time = "2026-08-03T21:19:39.809Z" }, + { url = "https://files.pythonhosted.org/packages/db/e2/7e8109f65445bdc673a7b54f02c677de462db75674220fd1335efc8eb598/cffi-2.1.1-cp311-cp311-win32.whl", hash = "sha256:f8ec5e643a9a937f64e1999eb9f75d072263751912dc5cd06d3c85f8f44be7c3", size = 174470, upload-time = "2026-08-03T21:19:41.246Z" }, + { url = "https://files.pythonhosted.org/packages/73/c0/77ba02423c2f7d7091143c45cd49e0e6575c4c1967394bb542bd923a9b74/cffi-2.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:42f6930c31dc7f50732c9ae793c2786c7b6b044195967bbdde40bb9be81c4cc0", size = 185096, upload-time = "2026-08-03T21:19:42.615Z" }, + { url = "https://files.pythonhosted.org/packages/7c/47/9f1f85f9672ceda4984dc6c4f8824e8558992a2972c3d3c81fb8eb28d4ba/cffi-2.1.1-cp311-cp311-win_arm64.whl", hash = "sha256:c7659f22557c5a0bc4855cd635f55edec690cc008a40768527762cb9fb263455", size = 179941, upload-time = "2026-08-03T21:19:43.747Z" }, + { url = "https://files.pythonhosted.org/packages/10/69/43965eccfdead3b9220015fd1320e117be8c6ed01a62ffab76eeb752f5d5/cffi-2.1.1-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:c8c69575568085ba0b1b10c0249d779a214aea6f6522e949a0fc9fb0fcb449d0", size = 184821, upload-time = "2026-08-03T21:19:44.887Z" }, + { url = "https://files.pythonhosted.org/packages/54/7d/16e5a096677b5e313ca80cd5e5170efa3ea44624a82bb111925522da64b1/cffi-2.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f81b3b8f3d4e343550fa4baa0e479bba9f2d29ce9c2e9b51d1ce1718d7442fcf", size = 184719, upload-time = "2026-08-03T21:19:46.129Z" }, + { url = "https://files.pythonhosted.org/packages/56/e6/8941622732edec876dd17d0453dce07317ae96db34f2ec1436c9d3785986/cffi-2.1.1-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:811bd1e21d32de12efca32393a0ab3f5133b54fce9bd44b8bd77ab07da14bf6a", size = 214799, upload-time = "2026-08-03T21:19:47.218Z" }, + { url = "https://files.pythonhosted.org/packages/44/de/f98430906df1545ffde0d543dd124a7a439bc2cd32b36b9c53f805df7333/cffi-2.1.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:68e62fe11f30d5ca8289242866f0a5291402d8529ca2178ab8afc5c9694ae890", size = 222389, upload-time = "2026-08-03T21:19:48.331Z" }, + { url = "https://files.pythonhosted.org/packages/6a/5b/717f1526b9957b34456313c31645c5b82b8fb5c3fe9e4752999be7128bfc/cffi-2.1.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:4a7c934f7360e8cd64fe9efadcbd10c7c6364f531e432b9a4bf5ccbc9e0e8b50", size = 210249, upload-time = "2026-08-03T21:19:49.543Z" }, + { url = "https://files.pythonhosted.org/packages/64/b3/f8aa4f3e34986c7e4ec45072d1b1b9dd295b6b18007b45518d79726dd725/cffi-2.1.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:3143d81e29e1e20a9ce10901ec369012947876596f75a222235965f2b7ae832e", size = 208775, upload-time = "2026-08-03T21:19:50.918Z" }, + { url = "https://files.pythonhosted.org/packages/b1/db/dceb9dd5b231e1da801793f8acc9f3c52a7e1afe40bb1aae37e02b0faad5/cffi-2.1.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c1453022f490d2459a11819d83ad1d586e9ff65a12ac3e705ffebd46d3685dcf", size = 221822, upload-time = "2026-08-03T21:19:52.054Z" }, + { url = "https://files.pythonhosted.org/packages/a0/d2/6cd24ae3be000a634109c247d1475d62e5616d0dc78c82770942ec384248/cffi-2.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:208f941bb9d18e768138677f0a6d2ce01f590df56043dda1df1535ac57c88517", size = 225232, upload-time = "2026-08-03T21:19:53.109Z" }, + { url = "https://files.pythonhosted.org/packages/cb/52/3fa190537004dd7f0ab860a6dc7c0175b8667f68d1e618a46f5498d30250/cffi-2.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:210019b6c7cf07f081b4c54635c8cf744377001350e29cc0f81c4377b4797735", size = 223597, upload-time = "2026-08-03T21:19:54.515Z" }, + { url = "https://files.pythonhosted.org/packages/80/fb/0bb75b7039588c074b37ae99f40d9bfddf990ecb2fbc346ebccd2e56b9be/cffi-2.1.1-cp312-cp312-win32.whl", hash = "sha256:046bfc24911b37851ee1b51aab8bffe713d89c68c6a057b09484ce9fd5f69b4e", size = 175292, upload-time = "2026-08-03T21:19:55.566Z" }, + { url = "https://files.pythonhosted.org/packages/d9/79/615cc094e2fb508cade7de88d3b4f6c4ec2bab695c97bce9153dc65aadf5/cffi-2.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:f53e442b08449d42821fa4a4fba000095af9f62742a500f978a9f557ec44339a", size = 185919, upload-time = "2026-08-03T21:19:56.89Z" }, + { url = "https://files.pythonhosted.org/packages/70/c6/d0ea84713fe46b243a436a18fcd47d639732747e21635c8a27191b06dc30/cffi-2.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:7bde5e4cc5c10140859842b9d383af292b22639a4dffb725314baf45968cef80", size = 180093, upload-time = "2026-08-03T21:19:58.155Z" }, + { url = "https://files.pythonhosted.org/packages/9d/f4/035513d4117049066b4779dc3b7c0c0fdad175fa13731c9f4003f1cd1478/cffi-2.1.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:b5bdfd1c873d4e093aabc0ca84c4ca6dbc4f752afb5c86f146d9742580c9da2e", size = 194248, upload-time = "2026-08-03T21:19:59.399Z" }, + { url = "https://files.pythonhosted.org/packages/76/af/2aeb4dbb5fc41a04161ae9ff1518de7cec08e164f44a8ce6a4cf7fd2cd1d/cffi-2.1.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:31348097ff5bbe827ccc41795d4dd099d9f0625e7def00ee653c137a490c2a6c", size = 196908, upload-time = "2026-08-03T21:20:00.746Z" }, + { url = "https://files.pythonhosted.org/packages/a7/46/2e5fdde8555706dd98139a910ca11be02809f3f605ce956f655d0214e100/cffi-2.1.1-cp313-cp313-macosx_10_15_x86_64.whl", hash = "sha256:9d2055050ea716bd38b7f7f1579c275386646b4894c155a3e2f3cd62ed41b7c6", size = 184805, upload-time = "2026-08-03T21:20:02.02Z" }, + { url = "https://files.pythonhosted.org/packages/55/41/4c7042f317b9217502988f0873af87e16ad606dc20f84e546e3e6ce9764c/cffi-2.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:19ee6127ee34de7d83ce3d371ebc5ed91addbdcc39f9ab15ce4eb35a4e534971", size = 184764, upload-time = "2026-08-03T21:20:03.141Z" }, + { url = "https://files.pythonhosted.org/packages/43/1f/1c3d90d91811c8f86ced9ed637956c54bfe5b79ca98fe976d7f8c8979f6b/cffi-2.1.1-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:6a8dddef476fab96d066d578fc88526767b836ab5ab21754e1d5bf3879c31c7c", size = 214722, upload-time = "2026-08-03T21:20:04.377Z" }, + { url = "https://files.pythonhosted.org/packages/37/6f/3b5ce4c3b2192d250f04908f2bfd91ef34552ec8f7716a5d4abdb8d67bb2/cffi-2.1.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f16c709686a78c727bbbf059f92b0bf41c6fc60deec706d2dc19f529175a6125", size = 222369, upload-time = "2026-08-03T21:20:05.544Z" }, + { url = "https://files.pythonhosted.org/packages/02/10/4b3c75dde3d9663c9e02ba05c2668b954f671d4bbe346413ca8c696b295a/cffi-2.1.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:fcd22650c908d7b7da162bbfaab594a1227a15d1643a98c68b122ac642fa2264", size = 210175, upload-time = "2026-08-03T21:20:06.75Z" }, + { url = "https://files.pythonhosted.org/packages/df/62/14f74b9543e605d17701dc797b815958b8bb70b7624ce1b832ddad48ed6c/cffi-2.1.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:aa9511c62d14da7aacc9b4bf51f3f697a621e83b2d6919008243c3aad168eea3", size = 208670, upload-time = "2026-08-03T21:20:08.04Z" }, + { url = "https://files.pythonhosted.org/packages/95/95/86342356ff5953b3fb06f7ef7c5bee212d45e770abc7218d451b9148313c/cffi-2.1.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a931079504ecc49efed7744c476a5c343a92fabf66dec2db95edb1b2fdc770e2", size = 221824, upload-time = "2026-08-03T21:20:09.274Z" }, + { url = "https://files.pythonhosted.org/packages/eb/ff/7b3429ff53aafe931ed8a5fc69f481bbef7ba6de87ddcbb63d08f483f613/cffi-2.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a2d7755bef5a12ed488f4ef1f1b69ee9191d7396083b755a5d2295f6edb4768b", size = 225148, upload-time = "2026-08-03T21:20:10.7Z" }, + { url = "https://files.pythonhosted.org/packages/34/34/a95870b9221e09cf4f2ce3178b1a210abdfe63a1bd357da940418d7b8d15/cffi-2.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e0bcb7e0f677f543555d2adff3bf19c05f66cdb4796e5ff602442ab2fe3c4ef7", size = 223564, upload-time = "2026-08-03T21:20:12.165Z" }, + { url = "https://files.pythonhosted.org/packages/70/ea/839b50531021a647fb5e929f72cf97bc1ff702b5472166164b5b6e76b851/cffi-2.1.1-cp313-cp313-win32.whl", hash = "sha256:334644fbac4eff73d985a17a91226df55d0f394160c4cfb880e084c8f7161cac", size = 175263, upload-time = "2026-08-03T21:20:13.559Z" }, + { url = "https://files.pythonhosted.org/packages/60/a6/8b149b2c3f2e11aaa1618ef64500b45f50f22c57a977a4dff1aff1f91042/cffi-2.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:1aa5645c30469b09530c4ebca77ebf8f17618293c58f8549cb1a543a50236e7d", size = 185688, upload-time = "2026-08-03T21:20:14.69Z" }, + { url = "https://files.pythonhosted.org/packages/01/9a/11f687cb39d6a3504060d5242f04f48c735afb4d3d533958a20594890cb2/cffi-2.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:63bbfd5ded17c4840ac07cd8f1c21ba9d9708141f840b324f422f41b207e3973", size = 180078, upload-time = "2026-08-03T21:20:15.917Z" }, + { url = "https://files.pythonhosted.org/packages/d3/7b/d6bbf82b8b96e7391438898c42f5bd96dd02030fd5b64937d248220003e2/cffi-2.1.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:7dbb61fe3a7699468030f71bbe5f8a0e326a151daa91beb11a6fc1f980c55e1c", size = 194064, upload-time = "2026-08-03T21:20:17.148Z" }, + { url = "https://files.pythonhosted.org/packages/94/e6/bcc91b283be94735e268487a054004f0aa19947b6348fa367db53230abc8/cffi-2.1.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:f24fb43132a4c6b4cb4eb029492919b2db645be6808d738f244fd146c03c32cb", size = 196720, upload-time = "2026-08-03T21:20:18.268Z" }, + { url = "https://files.pythonhosted.org/packages/d9/99/c4b0c17cacdc9c3b8f280026286a9826d6a208c0f047591a3c3ce99b91fd/cffi-2.1.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d28630f5854ab07ab1fd4aba756de52326c82e6be15d414b12793f1975048b54", size = 184964, upload-time = "2026-08-03T21:20:19.708Z" }, + { url = "https://files.pythonhosted.org/packages/b3/a9/9db617d05d7367c1ad0ab00b3aa6e6f9281edd689b4ee9ea0e5a84e89c97/cffi-2.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:661c298b4821edebead0c91edd2b00374d67ad7c5a1f7a91d4442633b79d6a72", size = 184962, upload-time = "2026-08-03T21:20:20.833Z" }, + { url = "https://files.pythonhosted.org/packages/67/b8/b42132ca113dc567d37684437b46ca1dafc885902b02a110a02d5b511857/cffi-2.1.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:58acb8ab8e295e6c5ea12f888cbb13cf21511ef2a3303a23f4325c29d17fe5c1", size = 222328, upload-time = "2026-08-03T21:20:22.118Z" }, + { url = "https://files.pythonhosted.org/packages/80/10/c5c0cbf0a657aecf59ef511409734230bf556f05a0d6c9eed7aa5c0a0166/cffi-2.1.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:456a61fa52d579ebf9df2e9552ead5129855dbaff6c1e5a9b1bc408809bdc062", size = 209985, upload-time = "2026-08-03T21:20:23.401Z" }, + { url = "https://files.pythonhosted.org/packages/d5/6c/bfa0b87b03b9238148beca990292843c9396ba069b54496596594173de7b/cffi-2.1.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:a4f00aa42f75d6e4595e8866e748cc1705adc0cddfeb2ca86d0d03993d63ba03", size = 208530, upload-time = "2026-08-03T21:20:24.628Z" }, + { url = "https://files.pythonhosted.org/packages/e9/02/4e7d553a7ac4b4238b38b3c1b80d486e9d4436f8d2acbf87a0997fe3f402/cffi-2.1.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b0431303acaea1089ad4b3e9ce4e6518193def1118d4073ca848635ee4ea2e96", size = 221525, upload-time = "2026-08-03T21:20:25.758Z" }, + { url = "https://files.pythonhosted.org/packages/82/1d/a4aaf9babd75acb4d5f223bff71533bee748dd770a382619a798960ee9ba/cffi-2.1.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:64faea20f4e2613363a1a9b9c7dd73058f3ecd00133a511e72ad7c511658f527", size = 225053, upload-time = "2026-08-03T21:20:26.985Z" }, + { url = "https://files.pythonhosted.org/packages/81/10/5dc0e7bdd18e22107054288283380fc97a06ae3f1656a106908d666a3c88/cffi-2.1.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5c58fe613dc5e5336357eff555824a314d8e43282600435c8d1cb6a7a2fedd13", size = 223213, upload-time = "2026-08-03T21:20:28.277Z" }, + { url = "https://files.pythonhosted.org/packages/0b/e9/d0061c364cde06ee43168a0d076ac1da512cbc380d44767b844ba34fe2b6/cffi-2.1.1-cp314-cp314-win32.whl", hash = "sha256:1a18a57b58cfb21fc28d72e876acf10eaed67a1ed96226f92af4df681d571c4c", size = 177682, upload-time = "2026-08-03T21:20:44.288Z" }, + { url = "https://files.pythonhosted.org/packages/a7/06/1c3e01e3ba14c39f6d10bfbac52753b7e22259e38088e5cfe1d704918690/cffi-2.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:3222ba5d678f80a030e6afbcc33dc1ae5cb45facabb61cee2c7016b8432fde48", size = 187949, upload-time = "2026-08-03T21:20:45.623Z" }, + { url = "https://files.pythonhosted.org/packages/87/5b/da4e39efe18eeb89cf580ea9cfc66b6a7c3eadb808fc0cc1d3a295cb5a5d/cffi-2.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:ab36d55f9ed2d067327667c2fea18dda018eb628dd6347aa01dda6cf1f5d3836", size = 182947, upload-time = "2026-08-03T21:20:46.955Z" }, + { url = "https://files.pythonhosted.org/packages/23/59/40338bf421c5accea1d45158170c87006ef1cd371b05c077e76476949728/cffi-2.1.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:7750c6449dff7864bb9bb27ddfb0267756189201a3afc911d82b3caacd70dfc3", size = 188504, upload-time = "2026-08-03T21:20:29.495Z" }, + { url = "https://files.pythonhosted.org/packages/7d/47/5ecf1023850036e674c77ec4de86182d309ae344e39e7cba984b7df5d647/cffi-2.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0beceaabe56af686895136a2de78db54ecd8e4046b236b8fd6d6cb61389e9bf2", size = 188259, upload-time = "2026-08-03T21:20:31.291Z" }, + { url = "https://files.pythonhosted.org/packages/2a/9c/92934c3bea9f785b23eba304538c0b4d37a2a96d2431eb3a1bc87a11aa19/cffi-2.1.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:49cbc70e6542d4ccccb936558d1064a8012541e78f821f955cff24e357776c94", size = 223864, upload-time = "2026-08-03T21:20:32.571Z" }, + { url = "https://files.pythonhosted.org/packages/4d/45/ba4c93527bc38616a8bd36488acb69a2212d60486794f0c1f318949bbb76/cffi-2.1.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:e2d65b31f36619cda3999b78b2aa9632e76b78448e7a56fc4240824200e7c4fc", size = 211538, upload-time = "2026-08-03T21:20:33.808Z" }, + { url = "https://files.pythonhosted.org/packages/80/e9/b6ef565e452acb932fb0cb5443f44a78efbd1233e566f02b5a83855e9115/cffi-2.1.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:28907ab9bfb6aa13184cfc17c6b8e1023c5ab6fd7076d8c20a35e59fe04f8f29", size = 210688, upload-time = "2026-08-03T21:20:34.974Z" }, + { url = "https://files.pythonhosted.org/packages/9a/95/eff5f0cee78d2eabc7eebffec40d3fc1876b5f3c95582e018bb4b99601f2/cffi-2.1.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:51b31d1c98274844cfd7838ce00bfc27c7423a4dc00fc0772fc3331c2cc90676", size = 223803, upload-time = "2026-08-03T21:20:36.564Z" }, + { url = "https://files.pythonhosted.org/packages/fa/01/579d39fb8bef00a335a23d83757b44feb24cd6345a2c451b64cb67b9c362/cffi-2.1.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5e7cecbaadb83884793e05828cee59b210b24583b9c7425d0ba6a754fe22eb4e", size = 226763, upload-time = "2026-08-03T21:20:37.816Z" }, + { url = "https://files.pythonhosted.org/packages/8d/b0/0b44f47c60b01b57b6e2bbd92343f13a85a1d93bc46ccf6e47e244acd99c/cffi-2.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:25792eac27877609e7bb06d42ff88278a6624fff2ba9bbb523c09616b117e80f", size = 225688, upload-time = "2026-08-03T21:20:38.959Z" }, + { url = "https://files.pythonhosted.org/packages/eb/d2/3b7176cb570a1d3e27faf67b72f591af508036e0d8b2be2ef9af9e8c84bb/cffi-2.1.1-cp314-cp314t-win32.whl", hash = "sha256:8ef53b2de9bcb9197d31854256575d59dbac0cba72ac627bb291ef5eceb74be4", size = 182868, upload-time = "2026-08-03T21:20:40.388Z" }, + { url = "https://files.pythonhosted.org/packages/56/78/31f00c1bcd97c9bbf55f1bfdf5bc809a5de8887473e90bb9960dca825e80/cffi-2.1.1-cp314-cp314t-win_amd64.whl", hash = "sha256:616f097f2fe415bc92a247f02e11f634e1f9e9a83d327e3c915c15089c87869e", size = 194104, upload-time = "2026-08-03T21:20:41.725Z" }, + { url = "https://files.pythonhosted.org/packages/7b/1b/58496f2ed0a35de575250c02a43ab3cc2c04d494a88fed31c1cabc0fd176/cffi-2.1.1-cp314-cp314t-win_arm64.whl", hash = "sha256:ad2c86c495b899d862ea0f4b42891b8713a3bd45dd4105c7fd51c2a72f39f3a5", size = 186402, upload-time = "2026-08-03T21:20:43.042Z" }, + { url = "https://files.pythonhosted.org/packages/c1/8f/9ebe220eab48a093d1a5a5e339ab0dc7316eef3bb04d63c42f0251b61f50/cffi-2.1.1-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:dddad92b554513a31f272570678ba307fb9f618f05e3d4a5eacafff9eae03e1d", size = 194043, upload-time = "2026-08-03T21:20:48.179Z" }, + { url = "https://files.pythonhosted.org/packages/ff/69/844bad3ece306c4782c2ecb93597035b6690d48704b803914c199da1e8b3/cffi-2.1.1-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:da0e573f9f97159390c89d9f1a9e41908b66d408cc5b58d08cf3847d844c531b", size = 196737, upload-time = "2026-08-03T21:20:49.457Z" }, + { url = "https://files.pythonhosted.org/packages/1b/8a/af668013284634733f02d683458a0728739c7d6ddb5e14cb0c20832266fe/cffi-2.1.1-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:fb92203a88b3d3053034db775110081c49d28be6551923805e039924093761e4", size = 184933, upload-time = "2026-08-03T21:20:50.639Z" }, + { url = "https://files.pythonhosted.org/packages/0c/75/2f5207ff6d1a613133b23a5203cc0c2a628313b5eb3974d7956ae3c57950/cffi-2.1.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:2ae64be792b8966f2c69538199728b290e34726562896df1e5dc8ffd8d8188e8", size = 185002, upload-time = "2026-08-03T21:20:52.173Z" }, + { url = "https://files.pythonhosted.org/packages/e2/31/9e1313b0a6e30e91b3b3d3fff51ae99c857c07738e3afcce1f7334e1b7ab/cffi-2.1.1-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:507a24c282e0f42f8ed737cf048572cbf580468da5555764a8331735e9c736b6", size = 222271, upload-time = "2026-08-03T21:20:53.462Z" }, + { url = "https://files.pythonhosted.org/packages/50/e3/f6234a833e6e08c7007003074723c406559eecf9b48dfc97471e5a8eb7a0/cffi-2.1.1-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:246fa40ce8645a614ff682e0b70f37134e460eaf93a775e0cbe3cca585a67a80", size = 209919, upload-time = "2026-08-03T21:20:54.783Z" }, + { url = "https://files.pythonhosted.org/packages/0d/fc/5f74e293fced6edb51af3a46c4ccf6c23c9943774ecb375ddbd522c76add/cffi-2.1.1-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:471cee653ae88de62096552e6d24ccb4a5adb8c8c9f10b5054d0122c15bf2779", size = 208529, upload-time = "2026-08-03T21:20:56.066Z" }, + { url = "https://files.pythonhosted.org/packages/44/16/29e6d01b388bef055ecd6ca8244b3f4d336bd09e92d5d892187b9601084e/cffi-2.1.1-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aeae0e330c9f6acd681f647d46cefd30c29f93e3392882e792e82080c9691399", size = 221630, upload-time = "2026-08-03T21:20:57.336Z" }, + { url = "https://files.pythonhosted.org/packages/a4/18/fa7f1f6857d5eb88a4ca99ffcbfb7c387a287ccc154c64a73e86314745d7/cffi-2.1.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:42a494cee34437f05546455144f2b5d9ac09b1face62bcfce597d2e521066688", size = 225134, upload-time = "2026-08-03T21:20:58.675Z" }, + { url = "https://files.pythonhosted.org/packages/e0/9f/e8e3dfa04a1b4c241f8c91faacad872b4d4efd051d49764ad4e2fd4b9fea/cffi-2.1.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:cc572dace3f60ef98d7b12ff411d20f5362feb31a0439eab0085bbfd349982d7", size = 223197, upload-time = "2026-08-03T21:20:59.968Z" }, + { url = "https://files.pythonhosted.org/packages/f8/7e/8debeb04f1ab9fe2a6963964cd6f1aaf7192627b83926586a6a4e089c9fa/cffi-2.1.1-cp315-cp315-win32.whl", hash = "sha256:4f42141fc14250de6dde5ee7ea4432be017252d91f19c5ad043c084cea629cac", size = 177683, upload-time = "2026-08-03T21:21:14.901Z" }, + { url = "https://files.pythonhosted.org/packages/e0/31/5158704cc474ab65c1647932e88be78dc0873f47130e253be38bcaf13d01/cffi-2.1.1-cp315-cp315-win_amd64.whl", hash = "sha256:e6e8cff14d6fb0be70a09c0bdc58096f501952d04624ebf867e0e56da2df8960", size = 187897, upload-time = "2026-08-03T21:21:16.108Z" }, + { url = "https://files.pythonhosted.org/packages/cc/4b/b3a2da8570c704ffc0f9762cdc3ec0f02c8573798e0b5cf7f11c82bbb70f/cffi-2.1.1-cp315-cp315-win_arm64.whl", hash = "sha256:27350daa11d4f10c540e6e89dada4c54feb7256ad03e9a4dc075ebad7ba360d1", size = 182935, upload-time = "2026-08-03T21:21:17.271Z" }, + { url = "https://files.pythonhosted.org/packages/d0/ef/5443574510a1207e6f6bc38ba6e1f1de36cb48fef07b2728bb896a21f430/cffi-2.1.1-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:c26608d2222fb1e94487e4a387d85f13eb55d5ed725cb25a0c589ac4ee60e7bc", size = 188464, upload-time = "2026-08-03T21:21:01.163Z" }, + { url = "https://files.pythonhosted.org/packages/7e/ae/a56fa8c4686ad50e148fcbc8d3ae0d03915ff5c30d795058988c24118cef/cffi-2.1.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:4be96343e422f2dfcd12ab5c9f5aebe03f82f737c6bffeca6830b3875cb44aab", size = 188262, upload-time = "2026-08-03T21:21:02.382Z" }, + { url = "https://files.pythonhosted.org/packages/53/b2/6187f46f2912276a3ae284076109cc5c8680482f11f766ccf26db4a86427/cffi-2.1.1-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:937c0052c05a31ca1daf18de3158eed4dbfcb9cc107adbea227728d647be701e", size = 223779, upload-time = "2026-08-03T21:21:03.553Z" }, + { url = "https://files.pythonhosted.org/packages/8a/f6/c3ad28bd19f77047a03084424fbd4cbe997303267c14423737324be0385d/cffi-2.1.1-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:df423d40ee8654634421812bc3b196da3f9bd7d32929da813f8394c4348a5358", size = 211520, upload-time = "2026-08-03T21:21:04.863Z" }, + { url = "https://files.pythonhosted.org/packages/a0/cd/ccac9013a5bd9fd764de118674ab9c805b5ca10c19270d90ee273f8b2240/cffi-2.1.1-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:a730a083190634c65cca36ba5f489531576ebd79bcd5c8e172130f6453127231", size = 210673, upload-time = "2026-08-03T21:21:06.223Z" }, + { url = "https://files.pythonhosted.org/packages/52/86/2976131c639aead931c5bee5aba67e4b09fbeb8018b6f282f70803f923a7/cffi-2.1.1-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:363e05fa78e15116c3c32c210ee36884fd6b9afa6d440e47112c3bd511d64cb6", size = 223835, upload-time = "2026-08-03T21:21:07.539Z" }, + { url = "https://files.pythonhosted.org/packages/ac/0c/33a7aeab2f9c76918c52e084beb39c570db3588133412929e8ec06fab90b/cffi-2.1.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:770de9db11e84213beec501cfcaa013b019820ca881e03344dea5844f7876d94", size = 226705, upload-time = "2026-08-03T21:21:08.774Z" }, + { url = "https://files.pythonhosted.org/packages/e3/26/2cde30fdde421130bfc18f70395731a6e6b2053c6a1978a5258ff04e72fa/cffi-2.1.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:7da0c5eff80f0197f3b3d1232ec5a682a9325f4ae9016a78f5f5ca35f9ced1f5", size = 225539, upload-time = "2026-08-03T21:21:09.911Z" }, + { url = "https://files.pythonhosted.org/packages/6d/cd/a361394c94b2129d604bb846f624a8e88255a3ee33129c434a00d715e64f/cffi-2.1.1-cp315-cp315t-win32.whl", hash = "sha256:06c72bb76605a4b0cd0aad6930b69d4baf7dd5d806cfc409b824191099700e66", size = 182707, upload-time = "2026-08-03T21:21:11.226Z" }, + { url = "https://files.pythonhosted.org/packages/9b/b5/ba2b299993c26577d529b6ae29841f9e15b9fcf004d65f423f4fcf94ade9/cffi-2.1.1-cp315-cp315t-win_amd64.whl", hash = "sha256:d9c275eaacd24aa73f94ffd6de08fc3f932424d8b6c376f4bed7cde376fe7bc3", size = 193772, upload-time = "2026-08-03T21:21:12.39Z" }, + { url = "https://files.pythonhosted.org/packages/aa/29/35e016098c814cd93de9cd320c66b5bfba14dc6ecedd3cb518fa7c408c69/cffi-2.1.1-cp315-cp315t-win_arm64.whl", hash = "sha256:d18e5ac0f2f03f4f518d3e23db0f0cad7faa1da8620e9c09461d443bbf6e6692", size = 186360, upload-time = "2026-08-03T21:21:13.636Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e5/3f/143b048436775b0f76ac3eec145c019e8173ccc2885c8f20319b996d5e83/charset_normalizer-3.5.1.tar.gz", hash = "sha256:6117b84ea48435e5356dc737f5121485c30920ba43375fa7b434fd753df0eac3", size = 171764, upload-time = "2026-08-15T08:20:44.807Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/b6/034f6802e9c3f6418966cfabb7db8c9252cc2429c5098f41cc43af804149/charset_normalizer-3.5.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:eda059b6bc8bc0812d626fd91a7ce01bf583df0a61296eff390fd94141a34e30", size = 363585, upload-time = "2026-08-15T08:16:46.646Z" }, + { url = "https://files.pythonhosted.org/packages/d5/fa/6a7e2a7c4b5451912b8c417732df79574354443592a88d616de03da66ae5/charset_normalizer-3.5.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa2bb0b37202dca27175591f761108b5d34096ade1191ffe4808bdf6b1571488", size = 251189, upload-time = "2026-08-15T08:16:48.287Z" }, + { url = "https://files.pythonhosted.org/packages/a4/c8/ab42b07cfd82e919f427fcfaa7c41abae8242833ad1aad66d42bae40b669/charset_normalizer-3.5.1-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0b2b1b3fa5670c127b246df1d0c059defd41f689a868a3b9d79df9b1cac42d22", size = 239724, upload-time = "2026-08-15T08:16:49.67Z" }, + { url = "https://files.pythonhosted.org/packages/e7/80/b9348b5d3041209f98b4cdad7655766369233f1d533f4f4f7558e9717bec/charset_normalizer-3.5.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6e5e4d73d588ca5ed09df1b7dcd1b203d1df3c542e3f50d126c947d432b10731", size = 280078, upload-time = "2026-08-15T08:16:51.228Z" }, + { url = "https://files.pythonhosted.org/packages/82/38/083a24028304bc85bb9e376fed801178423dcbb67495f73b6ea0624e1894/charset_normalizer-3.5.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b54e7e13267d49ffbfe68e25b3cbd774dab38fa37238f71265e91b36146eb21c", size = 276650, upload-time = "2026-08-15T08:16:52.625Z" }, + { url = "https://files.pythonhosted.org/packages/0d/35/731ac04aa0a097fc1c97f0994c375bdb230c6c96619db794208fe664e9ce/charset_normalizer-3.5.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c7b742bf31c88566b4bb6335a7f393bb322e580b6bb98df7bd0c25e6e3519ce8", size = 262325, upload-time = "2026-08-15T08:16:54.085Z" }, + { url = "https://files.pythonhosted.org/packages/f5/28/c2028e7021fb89c6e56868ed0e387b8e9aa811abdd2ab3208d6578d2c930/charset_normalizer-3.5.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6ba32c4d2abf1d2fe7cf27d280f4cca5664233b0f885549c7761719eb977f486", size = 261140, upload-time = "2026-08-15T08:16:55.604Z" }, + { url = "https://files.pythonhosted.org/packages/28/f0/0c0ceec6d98b7daa62e361e418135d59685811d79ba11529aad5cdf15e84/charset_normalizer-3.5.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0722590aabf9dc6a6c0343d523c05458fa2b5047dbe6302fd526bb570600753f", size = 252791, upload-time = "2026-08-15T08:16:57.103Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3e/48f4cd187b1c33189d86039e9cbe4f92c05454175504b44ff81806d4d1bf/charset_normalizer-3.5.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:aa1099b956fb795e686d073568f6dc002a0bb89765ea6d5b055dd7d9bf1b116c", size = 240730, upload-time = "2026-08-15T08:16:58.418Z" }, + { url = "https://files.pythonhosted.org/packages/42/85/f9e22af69af67c54cce42be9455d9c81294f918b4ccc454db01f66efcac2/charset_normalizer-3.5.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:bd6c173f04743d483881bffa1478d5a4624475b8cd1d2194956a75548e191c18", size = 280791, upload-time = "2026-08-15T08:16:59.918Z" }, + { url = "https://files.pythonhosted.org/packages/fd/4c/9044135f42127630b6fa742feb51256353f6ab87a78f2fdd1de3de955a7f/charset_normalizer-3.5.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:f298e218441525d3794428b4c8b8fb8662c6d3ea79925d4807ee6b9a96a3bca5", size = 259598, upload-time = "2026-08-15T08:17:01.421Z" }, + { url = "https://files.pythonhosted.org/packages/ba/ed/1dd7cfebb4e75812934c49ca3b79757d11948053f7937ab7070c151f3c55/charset_normalizer-3.5.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:6e2912d4babbc65196ac13c2f53468dc57fb8b9c25ef913e8c59ddf7c6dc0e1b", size = 278217, upload-time = "2026-08-15T08:17:02.782Z" }, + { url = "https://files.pythonhosted.org/packages/bf/eb/239c84503cc9e3ba6eb34686a24bc66e84f3924efdd7e38e751a19f6bc10/charset_normalizer-3.5.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3d27167433c0d5f18dc850f07d0b3816221984fecdc405d6c157a6f0b8f8e9e6", size = 263417, upload-time = "2026-08-15T08:17:04.216Z" }, + { url = "https://files.pythonhosted.org/packages/37/ab/4e4510e1e288478e2c8333131d1c1382382ba8cd2165053c79e39d1da961/charset_normalizer-3.5.1-cp311-cp311-win32.whl", hash = "sha256:ac00177c4831ffa650f8609e4bdddd5fe09c03b1c0c47acece7e6ea20421598b", size = 181774, upload-time = "2026-08-15T08:17:05.58Z" }, + { url = "https://files.pythonhosted.org/packages/e3/57/32f0ccea59e8612057c61d6fd22ef2cb63cca93c9fe594094919696ac170/charset_normalizer-3.5.1-cp311-cp311-win_amd64.whl", hash = "sha256:f9b1e28d0e8dbfa858abdba91d6b547beaf2df1a59bec6da6faae7b96a4991a9", size = 206653, upload-time = "2026-08-15T08:17:07.075Z" }, + { url = "https://files.pythonhosted.org/packages/17/d4/b65c433fc521e58b5f54293982a5e51c05cb5f2dd3f1c7a6acb65b75324e/charset_normalizer-3.5.1-cp311-cp311-win_arm64.whl", hash = "sha256:ae31a1a1db2ee6cc2942fccaf695c934bc7f3db9f2133a3fef1f367cf1a4ab10", size = 185630, upload-time = "2026-08-15T08:17:08.502Z" }, + { url = "https://files.pythonhosted.org/packages/30/27/78873dc8b6a56357517b74b6bb9568b80450e7bb4f6ef7e3fa9d22aa0bd7/charset_normalizer-3.5.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:5b6d1386bf0096d26d3a863dc0a487a5b4eb9aa93cf5ba69683d29dde6b9d60f", size = 344456, upload-time = "2026-08-15T08:17:10.072Z" }, + { url = "https://files.pythonhosted.org/packages/9a/4c/be49ada26b1f0232d57aa89bbebf997a5cc2332a5616b6eca26ff680044d/charset_normalizer-3.5.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4582c27e8c889d64811987b5967fbd3ae0c823fe1fd933b543d55ac20bb475fa", size = 238530, upload-time = "2026-08-15T08:17:11.563Z" }, + { url = "https://files.pythonhosted.org/packages/76/84/6f1290fa07ae6978d3960caa3eb1b8019bf9284ab7c2297b00c099ef4250/charset_normalizer-3.5.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:1d1c7a53a6c2103925cdd6d7229f8c567379f211c869793df679f2e9f738c369", size = 230200, upload-time = "2026-08-15T08:17:12.919Z" }, + { url = "https://files.pythonhosted.org/packages/e7/a0/47b18adeed31c8f16ba9700f32c1b18594cfa09f47eb672a488c273c22bf/charset_normalizer-3.5.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e6621fb2a4988d6e53eedc455e5903e2679f3967b8acb3d639f1b63c14a2e893", size = 262222, upload-time = "2026-08-15T08:17:14.571Z" }, + { url = "https://files.pythonhosted.org/packages/38/fe/341861ac118dae06f3ec0eb487488af52128f2ef2faf0b11003944d22259/charset_normalizer-3.5.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7c0c10730342b0c9b35dd1d619beb8214e520bd96a1f870f452680b238aab3e0", size = 258951, upload-time = "2026-08-15T08:17:16.158Z" }, + { url = "https://files.pythonhosted.org/packages/6f/89/bb5108dc6c3651dca963f2b0a3ba19bbcb370c94e1b6d3e0e844a58e6dca/charset_normalizer-3.5.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b9af956078716df40d985fb0dfeb2c2120c5ca92ba4ff4b388acfd01cdc14d08", size = 248801, upload-time = "2026-08-15T08:17:17.683Z" }, + { url = "https://files.pythonhosted.org/packages/b1/ba/ef83ae3aca816393decfa3530976f38a79812d707b80b580ac33b83f9877/charset_normalizer-3.5.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f9f8405c2c758532c74fed975dbee57be1f31a6e865c031870c79a6ed3212ada", size = 244070, upload-time = "2026-08-15T08:17:19.191Z" }, + { url = "https://files.pythonhosted.org/packages/f6/0b/c5292a2462d69b7378ea89793bbb5b2b6fcf6f7dd6d1667f9619094ad553/charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:96fef3e886d6a9874b14f27fc193fbdc69d5d8035783d86aa4e1cea594e695f9", size = 240110, upload-time = "2026-08-15T08:17:20.547Z" }, + { url = "https://files.pythonhosted.org/packages/46/22/111e5be3b740d5c2a5bfcedb3d237b6591e5c2e82ae9d6ffcb121fe0909c/charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5d8531a6569d025f68e2321e7638fb7978f23db58e5f69f56913837aae03816e", size = 232836, upload-time = "2026-08-15T08:17:21.895Z" }, + { url = "https://files.pythonhosted.org/packages/f9/d2/d2aad6fe0dbb44b194bf3becb60f5a0ac48446ade999a47fe7bb41eb09a7/charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:aae2ee51122d3ae968a3837d97dc24a0aeebb0dea23694422cd172bd30017cd6", size = 262712, upload-time = "2026-08-15T08:17:23.727Z" }, + { url = "https://files.pythonhosted.org/packages/35/5a/337e4663a5eae6de99db940ee8066d4145caafb61327db62deda15313cce/charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:7235dc28fc6dd9d832ac7c7bce95367dedb85929f17368a0c2bee1e080b9acbf", size = 242977, upload-time = "2026-08-15T08:17:25.157Z" }, + { url = "https://files.pythonhosted.org/packages/ca/85/f82f8a92e31c7519410e2e1afdc630f28ec47490ce2c09a11c1a43cbb459/charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:4abdc5f9ad448c1ecbfae2974b820535d6bc6e7eef63babbab3d81cf46968c71", size = 260207, upload-time = "2026-08-15T08:17:26.602Z" }, + { url = "https://files.pythonhosted.org/packages/b7/52/643d11ffd60e9ac2fd1fb87e167a19285b9eefeff4a40e63c87cbfbeab36/charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ba501e667c17d8411f98e67a022d9604ef179aff0e459b7e292c796837c13573", size = 250562, upload-time = "2026-08-15T08:17:27.971Z" }, + { url = "https://files.pythonhosted.org/packages/62/16/46556278c2168d12df9da7fede5dc6fc70e60301b26a82bbeec238c9cfe3/charset_normalizer-3.5.1-cp312-cp312-win32.whl", hash = "sha256:cfa1c0cc3a8f9f53f1243a5a99ac36fd003880199383b37672e86ddda9cb07e2", size = 178507, upload-time = "2026-08-15T08:17:29.277Z" }, + { url = "https://files.pythonhosted.org/packages/9d/7a/4c6c298171e6b3e745633180ff59350fc0ca0db1ffd28df1e369e0579f71/charset_normalizer-3.5.1-cp312-cp312-win_amd64.whl", hash = "sha256:3617ac3cfd8b9888f145ad89dd6e692285834b0201c6074a5eeaad3fd4d668c2", size = 200551, upload-time = "2026-08-15T08:17:30.668Z" }, + { url = "https://files.pythonhosted.org/packages/cd/d7/eb95a042f0dd22e304b0b6472b154f3546a1a039a9ee89ccb2a7f61591fc/charset_normalizer-3.5.1-cp312-cp312-win_arm64.whl", hash = "sha256:88e85ab89cb822c1e635f51d6d32e488f94e002e70e2f492bdb8b945543f345a", size = 180700, upload-time = "2026-08-15T08:17:32.028Z" }, + { url = "https://files.pythonhosted.org/packages/bc/61/2cb6ad133dbbb449fa2d37ccae973232f4827e799af258d15e589a3d1e9e/charset_normalizer-3.5.1-cp313-cp313-android_24_arm64_v8a.whl", hash = "sha256:4f298bdadb8f0b9e5672877f647d1be9373ef5320c9e2f049795e26cad28b6a9", size = 211584, upload-time = "2026-08-15T08:17:33.597Z" }, + { url = "https://files.pythonhosted.org/packages/18/57/a305c968be1ca13f3dd1b32f445877e97addf55d80b65c7cb35fac82b777/charset_normalizer-3.5.1-cp313-cp313-android_24_x86_64.whl", hash = "sha256:88ca277405c2d3b71c4e1c2ee0e7966e807bcba86a69d11e19ba199d18ae4491", size = 223359, upload-time = "2026-08-15T08:17:35.022Z" }, + { url = "https://files.pythonhosted.org/packages/09/0a/d3646670292ce8d8f8cc11ac067d44885e697a5591f57a9221128da5e7b3/charset_normalizer-3.5.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:9362dd90aa7dab48c0054a21187791ccf05473f7dba5d92b8033ae62164675e7", size = 194464, upload-time = "2026-08-15T08:17:36.452Z" }, + { url = "https://files.pythonhosted.org/packages/de/93/d51ec556e01042fed6f993ea859311bc7917b466684182fbbceb6ca24762/charset_normalizer-3.5.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:977cdbd483a9cff38179bea4fd754289a6f2195c7abd414aba85410b3e66cc5e", size = 197676, upload-time = "2026-08-15T08:17:37.819Z" }, + { url = "https://files.pythonhosted.org/packages/a4/a0/562247944386f7d4ef94467e84876600cc1e0f1b93239aaa9213d2bc3cbd/charset_normalizer-3.5.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e90251c0c7bdd54a100a0dce3c07b7e637278c93af29dbf78ebb89a58c4bac7d", size = 340473, upload-time = "2026-08-15T08:17:39.303Z" }, + { url = "https://files.pythonhosted.org/packages/31/e7/1d994be1b93d41e9502b8b0460eaa88a1dd8df335df415db87d6c3e91ab2/charset_normalizer-3.5.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:94d78ecec2605a8d0398b0f365d5f12a63248438516f5dac536a5eff7337df4a", size = 240156, upload-time = "2026-08-15T08:17:40.66Z" }, + { url = "https://files.pythonhosted.org/packages/09/53/27923ce5cc6cbccb832037b27dca98882d9c53e9b69e866bbbef4aae7fc8/charset_normalizer-3.5.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d59b75732e9b6f27388e10c14b0259cc5f2e48c78627d185e6a177b58ad3cffe", size = 228246, upload-time = "2026-08-15T08:17:42.003Z" }, + { url = "https://files.pythonhosted.org/packages/ce/48/5a97e84d63af1d55c07439cb80e56d99a8efb4295700eb4e18c0d1615d2c/charset_normalizer-3.5.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0d929fc574b4d6fd9e7c0f5c2ede8716a41911923aa7fa5fce38e0818aa4a1ac", size = 263660, upload-time = "2026-08-15T08:17:43.627Z" }, + { url = "https://files.pythonhosted.org/packages/7a/c2/071575791dcc88316c0a9a65ce38897a82e4cfe4a325f0f7fe1b1ac47bcf/charset_normalizer-3.5.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:394fea06235c8543390050ed5f529187074b029fb027213f6c46ac11ab5d950e", size = 260354, upload-time = "2026-08-15T08:17:45.094Z" }, + { url = "https://files.pythonhosted.org/packages/fb/af/63240b0c0248c075c2535a1f1bd992821d8251b9f173abc13329661d09e4/charset_normalizer-3.5.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:62b55f6722735a6c472f88361cde6640608773d9443cebdbb51abf436a1fcdd3", size = 250638, upload-time = "2026-08-15T08:17:46.496Z" }, + { url = "https://files.pythonhosted.org/packages/4d/66/70dfad64f15be09c15ccfee81330a7e515895dbe296dd23114e9a231268a/charset_normalizer-3.5.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fa48b1b63d639f9483e0633e092f5851e2348c352f1f9bb6c8182f87884ef876", size = 244583, upload-time = "2026-08-15T08:17:47.963Z" }, + { url = "https://files.pythonhosted.org/packages/c0/24/ef36367d38b9ddd4bccbf72888c342e8de1f5ae506fa0b2dcf970e2732a1/charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c71fb0d56c920c269cd3e2e3fe7c610e3f1fdb21a6ce60efa6430ff63676cea6", size = 242038, upload-time = "2026-08-15T08:17:49.481Z" }, + { url = "https://files.pythonhosted.org/packages/db/ab/55e683ba0fff2e43adafc10daa3001eac90fdaa419a97227d5a7067eedde/charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:485a0d363cafefcd2538a73c7c838daa2035f09b2c9f9b5e3133f80c6aeb84c2", size = 233677, upload-time = "2026-08-15T08:17:50.845Z" }, + { url = "https://files.pythonhosted.org/packages/bd/67/0f40eaf8d1b6e7cf15e82382a2965efaca787fc1c2794b7021d37aaf5036/charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5c0ea61a470e070686aa30892fed79e297d2c8d0ab46b8bcdf027d38c51da591", size = 264491, upload-time = "2026-08-15T08:17:52.61Z" }, + { url = "https://files.pythonhosted.org/packages/5c/64/12b4c2a11ee8df4fcc518c78b0d93e3a92bd3d5253d1617ce74ff0e8c7ef/charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:90b7481fb62fbe172c558bc6fd1c4c98d82004a54a7551f20e11ac9bf0b8708c", size = 245196, upload-time = "2026-08-15T08:17:54.023Z" }, + { url = "https://files.pythonhosted.org/packages/37/2e/651d910af6d0fba325eee1cda37ec5443462ed25360e666c144166eb6091/charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:35fe081843b35aad20ffeccec3eeffbe637b15d14f3fb22cc1b59cd8ec17e93c", size = 261660, upload-time = "2026-08-15T08:17:55.491Z" }, + { url = "https://files.pythonhosted.org/packages/90/c6/b09e05e6db7f64338e0dc067c79577b1138da86c1e38369096851d96be88/charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fd0350afdc3aabd5576f60ea109228bd5538139713c7b094c5cd27c73a98bc6f", size = 252618, upload-time = "2026-08-15T08:17:57.025Z" }, + { url = "https://files.pythonhosted.org/packages/76/4e/362d4f9fdcdf5556fb2aa3ce7d4a58ebce03ed1ff03aa1d9aca8d02f13f3/charset_normalizer-3.5.1-cp313-cp313-pyemscripten_2025_0_wasm32.whl", hash = "sha256:9d9a0dc7cbe9bec24c3f767c9122c41fe5a1bc43f47cd099d00d393e09769de4", size = 140362, upload-time = "2026-08-15T08:17:58.425Z" }, + { url = "https://files.pythonhosted.org/packages/b4/d4/703be739b26acce318bd29eb3b25b7209e1b1f527f9eae3d1f1f01fdde2b/charset_normalizer-3.5.1-cp313-cp313-win32.whl", hash = "sha256:d63600d620ad0064c3a748b950ac5ea38a80190e5498532efefa4b7b3f1da1f3", size = 177755, upload-time = "2026-08-15T08:18:00.037Z" }, + { url = "https://files.pythonhosted.org/packages/8a/33/56d97ade41c8db611e727168c52ae46c9224c362ec28d4b65d7e9869e8da/charset_normalizer-3.5.1-cp313-cp313-win_amd64.whl", hash = "sha256:aea996a6aba25260827c9ea511d1addfde2da9eb686ac961838509086188b7e6", size = 199295, upload-time = "2026-08-15T08:18:01.506Z" }, + { url = "https://files.pythonhosted.org/packages/5b/75/5b20dd1e6573a01a08158fe104104fa2c8abf941745596954185726cd46c/charset_normalizer-3.5.1-cp313-cp313-win_arm64.whl", hash = "sha256:fd0a274c0e5f9a21565cd9d3dd749b61f96b7aa1e20a93aa1ba4029518f2e5c0", size = 179856, upload-time = "2026-08-15T08:18:02.929Z" }, + { url = "https://files.pythonhosted.org/packages/29/cd/2b812ce5e888f1ce69a5350281e58aab07ae64a958ecae8912f30865718e/charset_normalizer-3.5.1-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:774d157f112367ff4abd29019f38f023c24e00e56edc7829c20e358a5a913ad8", size = 212318, upload-time = "2026-08-15T08:18:04.403Z" }, + { url = "https://files.pythonhosted.org/packages/9e/4a/a6ee107430768a5334e6d63f31f148a04a1a491ef161a1ac9415a73f2fa8/charset_normalizer-3.5.1-cp314-cp314-android_24_x86_64.whl", hash = "sha256:26422d45fd13551cf564c58932f7d72b4f58b93b0fcf18c35ba6be12b46bb102", size = 224897, upload-time = "2026-08-15T08:18:05.997Z" }, + { url = "https://files.pythonhosted.org/packages/c3/d9/35ae3f64f29d0179c35c3baefe575904df2913dde519129c7f75995a2b1d/charset_normalizer-3.5.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:09a7bba9f739468c8e78c36a75c33768e53cb1959fc638f510454c14683f00d5", size = 194848, upload-time = "2026-08-15T08:18:07.397Z" }, + { url = "https://files.pythonhosted.org/packages/74/76/f2fc7380f056cc273a53af37f50d08ad54b2c59f61078f31432edcf1c2bd/charset_normalizer-3.5.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:4c9548dc78002099910abaebc0a72ac58b7d30931869e0351c09b507dff4ece3", size = 198163, upload-time = "2026-08-15T08:18:08.989Z" }, + { url = "https://files.pythonhosted.org/packages/e9/40/095ce62fa078483cccc1fa2b36e6bc9580b85422a20ee9f925341c50e44f/charset_normalizer-3.5.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c428c6c31eb5f4277d7f8eccaf767fbd548ddd5ce3c8b4f4cbbfab3d96b5904c", size = 341823, upload-time = "2026-08-15T08:18:10.458Z" }, + { url = "https://files.pythonhosted.org/packages/f1/5a/0e58b1c04a1596e0256f407274a92d5fb2ee21324409d1fab1da48a65b5b/charset_normalizer-3.5.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2f06b7eae9dbe77fe1d644ca244dad508de8d302870a43f3c559b521270938a0", size = 242458, upload-time = "2026-08-15T08:18:11.989Z" }, + { url = "https://files.pythonhosted.org/packages/22/95/b4618ce912e6db0b1aae89ba788e38e8a7eba0f3025cc66e8c0699f977b2/charset_normalizer-3.5.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6b7430cf5728e68f6c462254009a6ef4086e1bea43cf2f57aa9c55fb4f50ff96", size = 226717, upload-time = "2026-08-15T08:18:13.401Z" }, + { url = "https://files.pythonhosted.org/packages/8a/76/c681192bbda3d55356db5dadd64381d5202b37c6b598fcda5282e88b5d3d/charset_normalizer-3.5.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ab743e9bc90c1f73552ec33e10e3331315acd2c397b36065b591b0181de533cc", size = 266111, upload-time = "2026-08-15T08:18:14.961Z" }, + { url = "https://files.pythonhosted.org/packages/88/be/55127bfca72c0cff6c022488d140d7c5b04c771e3b72e9bdb4836d54979d/charset_normalizer-3.5.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f6f7deae3feb4edfa2efaf7c574fe88cbf055038a6abdb40188e4fff66d5699f", size = 263128, upload-time = "2026-08-15T08:18:16.515Z" }, + { url = "https://files.pythonhosted.org/packages/e0/91/39c3af510b0aa32bbda03374259200f28430febfd1bf5e511fe765282ce5/charset_normalizer-3.5.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15f024313246a4ed976c60f440bb8d257815513a681d212ff74fd46f7d715a90", size = 251240, upload-time = "2026-08-15T08:18:18.127Z" }, + { url = "https://files.pythonhosted.org/packages/1c/a5/cbe418bbc6ecdfc3e05a0116002897c4b403a5e838d697e64c78e9f0190d/charset_normalizer-3.5.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:823f82903d189af463d7df250ef1f7f696f3cee08cc8d91deb565e8d425f6506", size = 245282, upload-time = "2026-08-15T08:18:19.625Z" }, + { url = "https://files.pythonhosted.org/packages/cc/a4/689bb42e8e7cd492f3cb64907c6bc00ad247ec9a3628cd3f8eed126e8ae1/charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:01e93745f7f219b703b60ba7afead36cfc4242782be5af484673fc500df12da5", size = 244597, upload-time = "2026-08-15T08:18:21.121Z" }, + { url = "https://files.pythonhosted.org/packages/c1/ce/9962938e179cf9f699d3f1e7b3114b5d7642dee6a893745229f9dd04f274/charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:329fc3ccb63ad22d867d84c2adea759a64079a37ba4a343433b02c7a2816871e", size = 231376, upload-time = "2026-08-15T08:18:22.57Z" }, + { url = "https://files.pythonhosted.org/packages/85/54/46000450ada53bd9eac5429a2c8c54cd2d9b39c0c255f229aea9af0948a5/charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:bb57753e36e4855b8ca375069482250a6246372331a3e4f3407eaebb007443f5", size = 266715, upload-time = "2026-08-15T08:18:24.235Z" }, + { url = "https://files.pythonhosted.org/packages/3d/bb/618749d70f792b44252a777bf89bfb86823b9bbc1ea13fe8ce759b07f38a/charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:fce8cbd4997efeb450bd298b54f755dcdff18d496f7a5ddbb4867c6d7c88fdc3", size = 245848, upload-time = "2026-08-15T08:18:25.726Z" }, + { url = "https://files.pythonhosted.org/packages/7e/3f/ffb64458527c7668031d5eb095d978de561958dc9f5b53f8e488a533e603/charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:6c9cdde8becb25a7fde49924511aa2644d6f8081cc8df8e9452724303348d8e3", size = 264521, upload-time = "2026-08-15T08:18:27.193Z" }, + { url = "https://files.pythonhosted.org/packages/4f/ab/74a55fd803916a35ac461daf002708191aac19b546b80dc8cabfedc63d98/charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9ac4444d8d4fd4c4bd08bf451ed3167aa9e7ec6cdb41b648794f1d1103652e36", size = 253054, upload-time = "2026-08-15T08:18:28.568Z" }, + { url = "https://files.pythonhosted.org/packages/a0/2a/6a9034b7d3c60b17499afb482df5878bf9fa20b50cc3887d5ef017a833db/charset_normalizer-3.5.1-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:f03ac127268b43ef4fe9e6ab6794a6794b49485a0cc0c1db79876d2f33f75bc7", size = 140580, upload-time = "2026-08-15T08:18:30.214Z" }, + { url = "https://files.pythonhosted.org/packages/f3/46/1d362e1a00d035d66b9869e1281eee115907f7e390a16a07824ab5737360/charset_normalizer-3.5.1-cp314-cp314-win32.whl", hash = "sha256:1f5883d77fd409a261abb5dc8ccbe335720d798b1de4abb3b1d47ccbbc76b53b", size = 180325, upload-time = "2026-08-15T08:18:31.877Z" }, + { url = "https://files.pythonhosted.org/packages/7a/7c/4938c329b6a9d446f6a59aa2092ff7118f274209b5ed0e26893d1d30a63c/charset_normalizer-3.5.1-cp314-cp314-win_amd64.whl", hash = "sha256:c658c50ac0c98cd755a2dd50b7977d3bca7df401dcc47fbdfa87db53ef7d4e8b", size = 204175, upload-time = "2026-08-15T08:18:33.466Z" }, + { url = "https://files.pythonhosted.org/packages/ac/33/eeb384dbd8dec570661354592f4f2e1b2fcc92585624d146a000caf53841/charset_normalizer-3.5.1-cp314-cp314-win_arm64.whl", hash = "sha256:4bea7f8ebe90bbd7f0e4a2de42ca6924ba23e3e76418c408ff82f1d46fabd687", size = 184123, upload-time = "2026-08-15T08:18:34.913Z" }, + { url = "https://files.pythonhosted.org/packages/1c/6c/c73fa9d5a85f6ab05395de61c5f6984e0a9ff40bb5ff888d46dff02526c6/charset_normalizer-3.5.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:fbc597639158fd7c14d55e808718848319540f51b0e6746e3eefa59723a4a348", size = 381682, upload-time = "2026-08-15T08:18:36.349Z" }, + { url = "https://files.pythonhosted.org/packages/30/c7/63565f860921457feba93bae6c86fb7746deb4cffeed2f375cb845318146/charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e71c909f353863b2b89c83de2ebed71ea6d0df8a6ef65a128193c5e650766bef", size = 240826, upload-time = "2026-08-15T08:18:37.887Z" }, + { url = "https://files.pythonhosted.org/packages/06/ae/7ae8807410dfa33f8e6f1715740adeaafa8a816cc4cb33508f54b1f7c896/charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7ac76cf9afd34929d76eb7fcb63be476a4853d8a96f0dcf2d0db68a0cbdf9885", size = 227861, upload-time = "2026-08-15T08:18:39.315Z" }, + { url = "https://files.pythonhosted.org/packages/e9/a3/887c1642f0da26000b0e0652d91071113c0e72cea33952e225cf589f49a9/charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a3a370082ce34d0612f421e15fe011c53bb1feff21a26d06ad4fb244dab5a375", size = 260758, upload-time = "2026-08-15T08:18:40.88Z" }, + { url = "https://files.pythonhosted.org/packages/3e/11/e6f5b9a3d0e55b0ef7505cd3765cdd48f22db89994c947b316f52f801fd8/charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:256dd4d85d9e4dc595e2bc983c980e73f62ddeb3165c58b4c3dfe78c5c8548c1", size = 259950, upload-time = "2026-08-15T08:18:42.351Z" }, + { url = "https://files.pythonhosted.org/packages/1b/ee/e4e10a94d51cd1ee638aa7e00b65399e6b2a4e8376ab6d2eac9f95586671/charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:58d4aa13a59c969dbfdf9e6a9560e242cbfd9e8a8f50c2747714df1a423adf65", size = 249329, upload-time = "2026-08-15T08:18:43.914Z" }, + { url = "https://files.pythonhosted.org/packages/c4/25/d5f4198819e6059735a84e8d0bfb72dc33976da67b97adcd3fb5a5e07ec6/charset_normalizer-3.5.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0c6dfb5ca6723eeed15aa8e564a014d69fcb8812f94eef11fe3631e0508199f5", size = 243137, upload-time = "2026-08-15T08:18:45.368Z" }, + { url = "https://files.pythonhosted.org/packages/a5/e9/e925ca7569cf9fb9701fd82503fee73eea5268fdb856bdd64947092d3daa/charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c010f5581d9c612804cc59fcf7b524b707fbcb72828551237ab545bb5c7034af", size = 242820, upload-time = "2026-08-15T08:18:46.842Z" }, + { url = "https://files.pythonhosted.org/packages/34/17/672c251a888ed2aebcdd2fe830ad0104e25ff83c43f5c4f9c15e9fc6853c/charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:52ec005752a56ae79547a05c0139ca2501a0c866390b6115008456b9f0e7cde1", size = 230504, upload-time = "2026-08-15T08:18:48.353Z" }, + { url = "https://files.pythonhosted.org/packages/3f/fc/f6a85abebd42ce4da2f1db0aa56cc6a0df1995e318b3875d14401b8381d1/charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:2bced4061f000f7187254a02ad3433ae17eaf991747ceea2f478422590a5bba9", size = 263087, upload-time = "2026-08-15T08:18:49.859Z" }, + { url = "https://files.pythonhosted.org/packages/98/66/7c42677e739ba66746b297e2046918d793078094dc239e1e72768cffccc6/charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:9eea3ab2597a5e65fe65296e2d6a84570845a6b55532d90333d740d48bbc850a", size = 243269, upload-time = "2026-08-15T08:18:51.601Z" }, + { url = "https://files.pythonhosted.org/packages/de/d8/a50b79237f417af10f8c2a501ce8d1ca87829a22e69117891ca4ba20a69e/charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:496846868fea80e479324862fa877f02411f2fd0f83b79ccee2607aa68b2a032", size = 258766, upload-time = "2026-08-15T08:18:53.23Z" }, + { url = "https://files.pythonhosted.org/packages/2e/1d/0fc91aeaeb3c83b748f532399ce67cf84604b48297405d740000f7a9e786/charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:85d5855daafc240cc045c026d7a15fd198a09b0fc8ff6f5ecbb5297b509cb11e", size = 250814, upload-time = "2026-08-15T08:18:54.768Z" }, + { url = "https://files.pythonhosted.org/packages/ae/10/3d8c777cf9024615295aa1b808324ad5b4a77855869c00824bad74ffaf8a/charset_normalizer-3.5.1-cp314-cp314t-win32.whl", hash = "sha256:58d3e12c88e0950bca850ae1f7c256055c097639c2edb9eb123af9807d8b15e4", size = 191074, upload-time = "2026-08-15T08:18:56.305Z" }, + { url = "https://files.pythonhosted.org/packages/4d/81/ae557d3c44d1a1d688696d60563413a0866a91b7ebc50f20df838be3d8c8/charset_normalizer-3.5.1-cp314-cp314t-win_amd64.whl", hash = "sha256:acaf604462bf330b0d07e7a07c1d6e4adac79e5fb13e9c5140590542cafacc00", size = 216476, upload-time = "2026-08-15T08:18:57.889Z" }, + { url = "https://files.pythonhosted.org/packages/27/e9/61c01fb8b804692569c036b3fc50495814502dcf13a60649c6055390b02c/charset_normalizer-3.5.1-cp314-cp314t-win_arm64.whl", hash = "sha256:fdb8a068947befafba9952162645dc2fecaeb400e64584829ed5e9b2fbe21a7f", size = 194115, upload-time = "2026-08-15T08:18:59.418Z" }, + { url = "https://files.pythonhosted.org/packages/4a/4e/8544831ef59d8f27ce92c80871380fdacc8076a8a56ed62f82e54f991333/charset_normalizer-3.5.1-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:9085f87b0e38a2b92b8923059b4e8789fe40d9279712d15dcc670048d77079af", size = 342048, upload-time = "2026-08-15T08:19:01.054Z" }, + { url = "https://files.pythonhosted.org/packages/7f/a6/e3b46852424246065355644f4fb6dbccc0239a42a2eee27ecfc8957f0bcd/charset_normalizer-3.5.1-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2679de311c7946dde5d3b6f44941844133ff5c7cb86099c0061ab1e8901c20a8", size = 242997, upload-time = "2026-08-15T08:19:02.492Z" }, + { url = "https://files.pythonhosted.org/packages/03/3b/0cc9a26777334ab2f2e3089b948bbf4e4fe72ea70b897715ef6415043ec8/charset_normalizer-3.5.1-cp315-cp315-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:baf3775a2635e5a11fbd5e4e64ee69c7e86875d224a5c72aca4c141064589a90", size = 237014, upload-time = "2026-08-15T08:19:03.943Z" }, + { url = "https://files.pythonhosted.org/packages/8c/c2/027335f0aa337a2a2e121bac1ad88c4f02ba6053ea0926802784f3db11af/charset_normalizer-3.5.1-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ac8c94b6539074e0f40899301273ac8402b9b3e01c7b7ba269ff30340aaaf20", size = 266174, upload-time = "2026-08-15T08:19:05.598Z" }, + { url = "https://files.pythonhosted.org/packages/86/d3/e367787febe4e74769dec0f406f2c3c8d1b955fce5aee1fd0f94e8367a45/charset_normalizer-3.5.1-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8fe532b3c966d1fb794e0698e4589d0444017ae77fc0b31edea13c0e35bcc449", size = 263361, upload-time = "2026-08-15T08:19:07.251Z" }, + { url = "https://files.pythonhosted.org/packages/af/3d/391b193eb9f3e84b02f9314088c386debdc0debee843535aaea2e2c6715d/charset_normalizer-3.5.1-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5c84bec0ab5ae0c64bfe73a7d2adcb5ce73b467523fc27fd6a28ab2aa6cbe35a", size = 252143, upload-time = "2026-08-15T08:19:08.816Z" }, + { url = "https://files.pythonhosted.org/packages/2e/57/de221f1745a90d418199761967e2776bfe2c275a1194220985e8c1d37833/charset_normalizer-3.5.1-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:854066be00447fa8de2ccbbe893e2ffc4b123ef16d897af794c1e18bd4a714b0", size = 252086, upload-time = "2026-08-15T08:19:10.255Z" }, + { url = "https://files.pythonhosted.org/packages/c8/e3/d119f86a01f9331e8186175f24873b1d74a7ee9e2e4b4d68f9947dae5afd/charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:21b82d8082f6f5e7f456ef0bd16323d08de1266efbfeb476e64b2a91d1471a4e", size = 245231, upload-time = "2026-08-15T08:19:11.807Z" }, + { url = "https://files.pythonhosted.org/packages/26/de/d8e48c135ae480879539cdb179c8d3b50c7879497d75dd899b5763b69cee/charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_armv7l.whl", hash = "sha256:838648accb3a7fd9803fd45c87bce8509648eb0c11bc34e216141300977244f2", size = 241546, upload-time = "2026-08-15T08:19:13.416Z" }, + { url = "https://files.pythonhosted.org/packages/67/c4/217755fd1abc50d326c252922cd642002758095a81ff45010337b8b3ef65/charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:195ce897c6153c0700078142cf8efe3e6454ca4cf4357499e4078dfd83396626", size = 267033, upload-time = "2026-08-15T08:19:14.981Z" }, + { url = "https://files.pythonhosted.org/packages/b8/d7/34d8e404e358d2adcc5a228c2134643af00104c8fb0bf525f3688d756f05/charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:978eab16f55b4ab2c2a745be9a0a840bf8f09a7f227d9c76eb30214d078865a5", size = 252045, upload-time = "2026-08-15T08:19:16.618Z" }, + { url = "https://files.pythonhosted.org/packages/5e/fa/40414471acf0aa0692ca77305aa00e434fcd8288f0941c93c30e9a5f8f2f/charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_s390x.whl", hash = "sha256:cc0329df4caaceb950d2f580b5ac716a377f7059624a0bafaeaf8a218c6ed774", size = 264866, upload-time = "2026-08-15T08:19:18.101Z" }, + { url = "https://files.pythonhosted.org/packages/32/90/fcc850bae791abd2e0c041847f13e270aa08692a79f3e00de6d2dce1cb50/charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:687c9ca3035544b113bea2055e180af96fb63c0c476e22a9180f51925186e7b7", size = 253932, upload-time = "2026-08-15T08:19:19.734Z" }, + { url = "https://files.pythonhosted.org/packages/af/af/53afe99068b3c10b4cbae592a52ef72a7c92c0188440e83ee3a078fd8f75/charset_normalizer-3.5.1-cp315-cp315-win32.whl", hash = "sha256:706bfd38730a5ac7a365793269a00f4e988178cec121391f4248d84ad8c972e9", size = 180320, upload-time = "2026-08-15T08:19:21.37Z" }, + { url = "https://files.pythonhosted.org/packages/c9/bc/f46a132041b29e4a8779ed712d3df1bf112e94ca8de58b66d7ec2c0cf8b9/charset_normalizer-3.5.1-cp315-cp315-win_amd64.whl", hash = "sha256:92caef967d287a407085d61176fce4012b1dd62daed4eb6d5ceb26d3d2538712", size = 204174, upload-time = "2026-08-15T08:19:23.088Z" }, + { url = "https://files.pythonhosted.org/packages/a1/5d/9ed554480eda8e447b673648628fdc29574d23dbad01fe11837adedd1cae/charset_normalizer-3.5.1-cp315-cp315-win_arm64.whl", hash = "sha256:5fc45d653ea8c9a20479167e11d4a0f8cb2fa3470737ab6f9c827532313187b7", size = 184126, upload-time = "2026-08-15T08:19:24.471Z" }, + { url = "https://files.pythonhosted.org/packages/3b/32/9b8929bf384061ee1fe5d9c27c6f9776d3d824039ad4e14c88ec00c7808e/charset_normalizer-3.5.1-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:59171c6e45bf07d0d5cab3b0bf81d945035530f6873398b3b531c31184d46663", size = 381441, upload-time = "2026-08-15T08:19:26.038Z" }, + { url = "https://files.pythonhosted.org/packages/96/10/e9aa7923d3ddac652c99a1c5f7be494e737e151566a44abe018daf757f2c/charset_normalizer-3.5.1-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9dbdd9205662134957cf0c324f639bdc5031c0ca056e2369e238db75187c0f11", size = 241742, upload-time = "2026-08-15T08:19:27.532Z" }, + { url = "https://files.pythonhosted.org/packages/28/53/a2d249ebddf47b889a100c0bdcb61a2f9dbb8bc24ef325cc062e4f476877/charset_normalizer-3.5.1-cp315-cp315t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e4b018dc5a0eee4676e38fe84a47a427816c590b93b55d9025274ec4d6ffc2dc", size = 235298, upload-time = "2026-08-15T08:19:29.274Z" }, + { url = "https://files.pythonhosted.org/packages/7d/07/469f78af590f7d5cd48e20d8dbfa3d66deeff9ba37768c04d886b5afd45c/charset_normalizer-3.5.1-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ced3fdd71aaa83ce593746c2edb42b7a59cb4c19c8b5c407781c72e493aae55a", size = 262500, upload-time = "2026-08-15T08:19:30.955Z" }, + { url = "https://files.pythonhosted.org/packages/55/66/3bb56a47f7dcba014055b1a1d33c6f08bbe9c1e74dba154cfa25f90ae885/charset_normalizer-3.5.1-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:19a3dd5aa73cef1c99687c4fc57db016a9c17104ae1185da88ba566a5d3bebe4", size = 258888, upload-time = "2026-08-15T08:19:32.458Z" }, + { url = "https://files.pythonhosted.org/packages/ff/c1/2adc2800903fb013210349313b710a5376856578d9e33e6b9a1d8b36714a/charset_normalizer-3.5.1-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cc5d36d96478aa9c60654bd932525bf32964c62a7281eafdf16d85003a8d6004", size = 250243, upload-time = "2026-08-15T08:19:33.94Z" }, + { url = "https://files.pythonhosted.org/packages/95/b5/a18d0dd1157ab655cc2cb14a545f4a4784bbad70ab3502412e36097502d9/charset_normalizer-3.5.1-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:04368edf83514385ffc3e1cfd4546e595f4f1272dd23ba437a93a9cc3741d47b", size = 249871, upload-time = "2026-08-15T08:19:35.413Z" }, + { url = "https://files.pythonhosted.org/packages/ad/c3/525f508cd1e58d0450ac55ed40ac75bc3a97482c59def5278456a5fbf03c/charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:9b5db6052055d34d41230fb78d7c439c23dc536a9896f6cb039e8dd92cfc1263", size = 243580, upload-time = "2026-08-15T08:19:36.886Z" }, + { url = "https://files.pythonhosted.org/packages/7c/c1/49a91fe7e97c8140094ca5c64161ab623a70d9f636bf834eace14048acb5/charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_armv7l.whl", hash = "sha256:252d099029bcbea642f2a06c4ed5046bdf8b5a8150b64afa5e027e88b106e5ee", size = 239807, upload-time = "2026-08-15T08:19:38.392Z" }, + { url = "https://files.pythonhosted.org/packages/d3/58/56a48c296601274c4689b864a8e2dfb209b81dfcb39472753ce95eea662b/charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:6199d5606e2bbf2b096cf64d03f8b6790c91081d5ac866b8e7bb6422738cc60c", size = 264083, upload-time = "2026-08-15T08:19:39.856Z" }, + { url = "https://files.pythonhosted.org/packages/10/4c/dc48409274a1817ff349711d26c62aa0c597df865d4d69ef79160c859193/charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:77efcff2b23071c349402ac1066667a3d011f62398d81408c9b88ad991747c9e", size = 250317, upload-time = "2026-08-15T08:19:41.53Z" }, + { url = "https://files.pythonhosted.org/packages/81/58/d325912115caec62d6bdd77bbab5e0b7da5d234a9f20affdffcbcb530d0b/charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_s390x.whl", hash = "sha256:a5cbd90ecf0fc62e64726917ad083b73001f0563657a87ec3c0b504e277dc90d", size = 258173, upload-time = "2026-08-15T08:19:43.07Z" }, + { url = "https://files.pythonhosted.org/packages/34/f7/b13b1ccae2c8ec63980d13be1890eb73f8aeabbfce02a24aabc0908788f5/charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:4d26f14f041e83dd8edfd61f4cd4fa7285d31798b5bf1f28e70c367ba6c41d61", size = 251960, upload-time = "2026-08-15T08:19:44.587Z" }, + { url = "https://files.pythonhosted.org/packages/1e/25/ed3f9919c5aef8cc818be1f972f565f7610d7b2076b8ebb98839516ffc3c/charset_normalizer-3.5.1-cp315-cp315t-win32.whl", hash = "sha256:ac13b004224fb341e1e25a1ed5e19d32f57cdb2a403e01f003b46f051a550f6f", size = 191186, upload-time = "2026-08-15T08:19:46.293Z" }, + { url = "https://files.pythonhosted.org/packages/69/d5/43c2b3e9d8267092b913eb8b0603f0f71993c395632886bd37a7223f96cf/charset_normalizer-3.5.1-cp315-cp315t-win_amd64.whl", hash = "sha256:35aea775dc2bd5f54cd84a1cd2696cc3207c479cb9cf0bd346f0d343e4300ddb", size = 215947, upload-time = "2026-08-15T08:19:47.853Z" }, + { url = "https://files.pythonhosted.org/packages/a8/76/9aad3e9c8865e5e0efa9a7f6f81c37a67635a985145ecd44528a81e088ee/charset_normalizer-3.5.1-cp315-cp315t-win_arm64.whl", hash = "sha256:fb78f6e7fcd8ad785d28cd577168bc1aaee827b25bb8755638f694794ea98f0a", size = 193909, upload-time = "2026-08-15T08:19:49.383Z" }, + { url = "https://files.pythonhosted.org/packages/5b/97/fb4e82231aba271ffd775a1b4993b0defc4e3059f286ae41d9433409fe85/charset_normalizer-3.5.1-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:41876ee62a3dddf48ff1121ad8f0798032aa03f2fd35f21f34a4cab14f18d8d2", size = 331467, upload-time = "2026-08-15T08:19:50.959Z" }, + { url = "https://files.pythonhosted.org/packages/9f/2f/fe3f187327aac18e2d54e9d2b08e15d27bf9b642d9e51c219f130fc34d1a/charset_normalizer-3.5.1-cp37-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a6dac12ff6b846103483683f60c5f8fee205121adc58ffd87e90a90a3af69e99", size = 253057, upload-time = "2026-08-15T08:19:52.654Z" }, + { url = "https://files.pythonhosted.org/packages/d7/c7/9e48cee5c161fe24da823b61bf381921d77cb994a0a4de148e95018c1984/charset_normalizer-3.5.1-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cee5dd7c6fb5dd52a0fe2a740f9bc6e3593f5f8b1788bde49de02086f30182b2", size = 240930, upload-time = "2026-08-15T08:19:54.163Z" }, + { url = "https://files.pythonhosted.org/packages/49/e0/716601f3cc69be7b198951150c75ead1ece33c3c8036ff6ffa46029659a0/charset_normalizer-3.5.1-cp37-abi3-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:343fb4f2821043bd87095f7b08a1a181febc8e36ac64212143bbfd0a0e1bc235", size = 230822, upload-time = "2026-08-15T08:19:55.807Z" }, + { url = "https://files.pythonhosted.org/packages/d3/05/71bfc5caa0abcc45aea1f6a4d50ac68e59605ddc7666fe8494f4cd229665/charset_normalizer-3.5.1-cp37-abi3-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ae4a097991662cd4fff0ddc74e0fe7874f82e00042fa0ea00855645ed0c79598", size = 260037, upload-time = "2026-08-15T08:19:57.312Z" }, + { url = "https://files.pythonhosted.org/packages/c3/92/de7e32ed05341e7a9c4c877c318418197b7f2d66a3b68d561bf2ac57ca3e/charset_normalizer-3.5.1-cp37-abi3-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4b599739b93b2cbeded49645ae3c8d1405c29ddfbceac1545c87a3f9580a9e96", size = 255097, upload-time = "2026-08-15T08:19:59.056Z" }, + { url = "https://files.pythonhosted.org/packages/f5/7b/ade0a122600319dfa0b1000ab0f9731c94a817904cf3c5de408c73a4ede7/charset_normalizer-3.5.1-cp37-abi3-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b39b69b347e5e47a3b5b8cfc005c68c1ba347474e3960236c4944a8ecd174962", size = 250166, upload-time = "2026-08-15T08:20:00.612Z" }, + { url = "https://files.pythonhosted.org/packages/75/9c/019fbb9f4834491a160951349b1a3714439376f66e5f7cf18b4f18f0c7aa/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:a2028475ba855475b8b4d3cfeb4994269c967aea8b9892dfba907f4263a863a3", size = 241821, upload-time = "2026-08-15T08:20:02.321Z" }, + { url = "https://files.pythonhosted.org/packages/2b/b8/11d4840bfc99330cc7fbcc2681ee5a044553a6e77655508d8f9b2bff7b34/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:36047af20e17097c3bb9476c2b7655f2f7aa51322c0ba58c07695bedf755a950", size = 232529, upload-time = "2026-08-15T08:20:04.008Z" }, + { url = "https://files.pythonhosted.org/packages/18/96/2b3a21492d9f65171ac75d872f5018260013d00bfa0ff70ec9f179148cbd/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:4c4fb141a727957c93edfe5c32a26ceb6b5f6461d67146e2d39f51e16170bea8", size = 260348, upload-time = "2026-08-15T08:20:05.877Z" }, + { url = "https://files.pythonhosted.org/packages/d6/aa/a69a2028e8bd052476c245460ab19d7de595de084dd968f2d75cd50c3e25/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:2f293479cce755c75f1697e87c409b7ae4c555c7dfecb6e988ad13abba943031", size = 247234, upload-time = "2026-08-15T08:20:07.487Z" }, + { url = "https://files.pythonhosted.org/packages/35/8a/3d130aeabcaf3d2466af76b7b141c08d9e89c9016ab4b7cdd0f7dc2d1c62/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_s390x.whl", hash = "sha256:3588e376b3ea2eea84976f67273d679f229e24c66dce7b82ae45aef04ff6e072", size = 256917, upload-time = "2026-08-15T08:20:09.142Z" }, + { url = "https://files.pythonhosted.org/packages/80/c2/a7379b840292d0c1ab9fbd17d1f3967aa81794dc95bc74be8999d7fedcf7/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:e199fb99720074809a7720f1c0b4d919eea8b87e88713e0f8f602f7bef543d9d", size = 254846, upload-time = "2026-08-15T08:20:10.727Z" }, + { url = "https://files.pythonhosted.org/packages/01/65/d43b714731bb2f40d4053dfa00ecfc1c5a301f8e3316c5db3a09af59fe94/charset_normalizer-3.5.1-cp37-abi3-win32.whl", hash = "sha256:dd732602a7009217f658d5863d12d79d373a4de0eebc111094bcdd3bb8e0a6cc", size = 174216, upload-time = "2026-08-15T08:20:12.334Z" }, + { url = "https://files.pythonhosted.org/packages/35/4f/b911ed898b26a09789eba9c9200c999aff6c61b4bafaf4838e56d1a1e1a3/charset_normalizer-3.5.1-cp37-abi3-win_amd64.whl", hash = "sha256:70055ff39b97c99e7ae40ea3e393fb62aa2e44dbd9b29f8d14f42fb0025c3959", size = 199764, upload-time = "2026-08-15T08:20:13.908Z" }, + { url = "https://files.pythonhosted.org/packages/f0/a7/920baf467bfd9bf689f3b318340f37aee4572a71f162bd8db51da55ba4fa/charset_normalizer-3.5.1-cp37-abi3-win_arm64.whl", hash = "sha256:87e4f41d375c0b9be2fb5251aee4b8a689169e134535aed81bf085c3b647451e", size = 287318, upload-time = "2026-08-15T08:20:15.551Z" }, + { url = "https://files.pythonhosted.org/packages/cc/61/d01fc49b8dea277640b55a9e15960dbca9fdc8c9fde18e572d39c59f4019/charset_normalizer-3.5.1-py3-none-any.whl", hash = "sha256:6df0ec430f9a831772c23ca5a224cba36517a58a84bb32c32bb59a9fa67c47f6", size = 68658, upload-time = "2026-08-15T08:20:43.306Z" }, +] + +[[package]] +name = "click" +version = "8.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/0e/7fa0ef50764b67090eca4114772a2abf8b6148198475e54c660b97caeee6/click-8.5.0.tar.gz", hash = "sha256:ba0d2089de75ea0310e2dde03160e6ca10009947fb95a182f9b54021bb272e34", size = 382235, upload-time = "2026-08-26T13:33:14.56Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/58/50/6c0d534c5f134586a8e1ba4e330569e32f057e33372ae556463212fb4cd3/click-8.5.0-py3-none-any.whl", hash = "sha256:255bc9599cf7748b4b1a446ccc735421bd08a2ae529a8b88597d3de5664ee360", size = 125251, upload-time = "2026-08-26T13:33:12.928Z" }, +] + +[[package]] +name = "cloudpickle" +version = "3.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/52/39/069100b84d7418bc358d81669d5748efb14b9cceacd2f9c75f550424132f/cloudpickle-3.1.1.tar.gz", hash = "sha256:b216fa8ae4019d5482a8ac3c95d8f6346115d8835911fd4aefd1a445e4242c64", size = 22113, upload-time = "2025-01-14T17:02:05.085Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/e8/64c37fadfc2816a7701fa8a6ed8d87327c7d54eacfbfb6edab14a2f2be75/cloudpickle-3.1.1-py3-none-any.whl", hash = "sha256:c8c5a44295039331ee9dad40ba100a9c7297b6f988e50e87ccdf3765a668350e", size = 20992, upload-time = "2025-01-14T17:02:02.417Z" }, +] + +[[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 = "confocal" +version = "0.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "pydantic-settings" }, + { name = "pyyaml" }, + { name = "rich" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b6/b4/fb7cbb267b9ba4b8934eaa6f6ebfffe5dacfe9fb804562781b284746d98b/confocal-0.2.1.tar.gz", hash = "sha256:859ae1218810a708e9fb5d9a98ffd63528fe986ca9ad561a99a870cf165c0ccc", size = 11634, upload-time = "2026-07-02T21:41:45.248Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f9/9e/c7906e206c51c33586c21e98feb552f75b9ce3b7ab9a11872605842267e4/confocal-0.2.1-py3-none-any.whl", hash = "sha256:d1e2e4199cfea5b4f6e048a692326692ccefa3e4818cde3708d99e0ed50bebd6", size = 13510, upload-time = "2026-07-02T21:41:44.255Z" }, +] + +[[package]] +name = "cryptography" +version = "50.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bb/ad/5d6702db60b1e40b41ef513b6967ff5848f307d50f8449baf1634f5908f1/cryptography-50.0.1.tar.gz", hash = "sha256:5dd9bda1c12b4162f6ff568eeb5e0ff956c28d14406e875cfe8a63a2d414ff20", size = 880381, upload-time = "2026-08-25T19:45:45.499Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ba/19/797e2aaac9df6a66f1550f49979dc1b1e39ecd2077501c30efa81e8d5d67/cryptography-50.0.1-cp311-abi3-macosx_11_0_arm64.whl", hash = "sha256:b8f852c65863251b9e3a1b8c150ce21e59b522dbb6a7d4bc80e680d38388e986", size = 4010153, upload-time = "2026-08-25T19:44:03.155Z" }, + { url = "https://files.pythonhosted.org/packages/90/34/9ce9a62ed9dc82ca9fd6a34445b6904af56e5f38b3eae2ed32e49c36053d/cryptography-50.0.1-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:53e279950892dc102c6b4e52af03ae5ea92fac572a1ddab78ca73a997f62b69f", size = 4723133, upload-time = "2026-08-25T19:44:05.461Z" }, + { url = "https://files.pythonhosted.org/packages/57/26/e6d4fc8512a51a5f9ee7bfdbfb853bce1197087df40c9ad993ad370b846f/cryptography-50.0.1-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ff838d62ec1bfce4f9ba7fa16f4a7b554cd8d0c299e6be37502161a660c84eef", size = 4712478, upload-time = "2026-08-25T19:44:07.375Z" }, + { url = "https://files.pythonhosted.org/packages/e6/de/d3cdc2815697aae84126cbd6a030ca7b6b452e28a88b501b836bd3aa7a86/cryptography-50.0.1-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:e74591e283fe6eb956416c929eb58262a719fe0311fd9054c62c3350ed8760d8", size = 4730726, upload-time = "2026-08-25T19:44:09.294Z" }, + { url = "https://files.pythonhosted.org/packages/55/32/38c0d344b98c06d34b5df8946565a9c0d6dbf32c8e0730a7f05f0a3c6cab/cryptography-50.0.1-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:5fe002589592ed749ce77fe0695fcbd3500dd61d7d6db5858a7544c612fa8e45", size = 5353524, upload-time = "2026-08-25T19:44:11.96Z" }, + { url = "https://files.pythonhosted.org/packages/e1/1b/82f0f0d8858d4432be1af790477edf62aef90324041aa07c57e57bef1af7/cryptography-50.0.1-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:51593d180cf6d179bde5c5d065bed81386b1f381656ae7d042b7ffc87a9895ad", size = 4746720, upload-time = "2026-08-25T19:44:14.051Z" }, + { url = "https://files.pythonhosted.org/packages/29/ba/042ca458b8c64348c768284b5d23e69b92ed53d057ab779fee628564676d/cryptography-50.0.1-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:359e62deae718bce96170e223fdcb6357e4fbd3bb7a3a75f4430763532560e49", size = 4361866, upload-time = "2026-08-25T19:44:16.167Z" }, + { url = "https://files.pythonhosted.org/packages/39/3b/e96c1ef71edef71057c7e3c3d982ce8fda554e0c52d0cc19c18845cde3eb/cryptography-50.0.1-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:e2ca8fd1b6b4b82a1c4cb02841d0837e3c12336c2e24b520ab8ab3b969733d8f", size = 4730028, upload-time = "2026-08-25T19:44:18.085Z" }, + { url = "https://files.pythonhosted.org/packages/e3/38/45abd72ef63f2e7d0754a6cacf97bd8b69512ace7f6130d24c39ece65da2/cryptography-50.0.1-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:76de83fbd91ac49c0feaaa983d0748fd7a53176afac5fb3bf7478d244f0eb527", size = 5308405, upload-time = "2026-08-25T19:44:20.197Z" }, + { url = "https://files.pythonhosted.org/packages/85/66/6ccca4722987ddedaa7fc9c3f4708af7431f5535666c174350830888c6b7/cryptography-50.0.1-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:51afcfceb15597cf2635068e4ac9a56b2abde622edde17f37d85fd7b5306497a", size = 4746230, upload-time = "2026-08-25T19:44:22.376Z" }, + { url = "https://files.pythonhosted.org/packages/13/0e/b1f92e013228111413f2e6743948b80bc24dfd3c1b87ba98ceea16f5df89/cryptography-50.0.1-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:be224a65493ec5b74a158ff22a5522ce4a5ca1e543c647a3a4730d4a09e5f959", size = 4862596, upload-time = "2026-08-25T19:44:24.472Z" }, + { url = "https://files.pythonhosted.org/packages/7e/22/c3654cccc856e9d682817b04ac3ee79731cb09ca6f95996a95c904de2883/cryptography-50.0.1-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:9ebcdd5519be9b652a46f507817a74591774fc3d6923ac364e4dfa64e36b291b", size = 5014082, upload-time = "2026-08-25T19:44:26.709Z" }, + { url = "https://files.pythonhosted.org/packages/42/8b/cb12b1b60c91b074ca6bf0fdd59aa8f10d8bc5f73af8faece86ef0421b37/cryptography-50.0.1-cp311-abi3-win_amd64.whl", hash = "sha256:aed8db4f6d71c51efb89530e12d9464e7bf2923d46c3205dc794a2a93f8c0648", size = 3842826, upload-time = "2026-08-25T19:44:28.784Z" }, + { url = "https://files.pythonhosted.org/packages/5b/f0/424cb557d99aa86ac55da5e2add02e2882e44047b6264f93ade1b975a993/cryptography-50.0.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:30a125032e5642a21ff816e021152bd4e7e94f03eff3f4b7fca41cd22bc3110f", size = 3973525, upload-time = "2026-08-25T19:44:30.7Z" }, + { url = "https://files.pythonhosted.org/packages/4d/72/3a2711d967977ab5fc80b782837c7e8d1ac7445e764c20c381a265c57ef3/cryptography-50.0.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a0b1a59e3a089064a0ec309e9428c8e3ae4e161419d20ac33600767e83fc658a", size = 4708817, upload-time = "2026-08-25T19:44:32.773Z" }, + { url = "https://files.pythonhosted.org/packages/b4/f2/bb1f56e10815b789df0b409a69fa4992ff3d3fef9c72747f4a6b26fed38e/cryptography-50.0.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8921d58f426793c5f1b47f0b59575780de9a095214958d0eb37d909593db8367", size = 4697300, upload-time = "2026-08-25T19:44:35.144Z" }, + { url = "https://files.pythonhosted.org/packages/08/bd/ed5396be499ffcf8807a585bfe38b71a1fbdd1c342b4f9b6d0ef5162a946/cryptography-50.0.1-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:a8f40ea47330e71b594a7e246898f93177c259490c63183dbaf9e571d71ed9a5", size = 4716039, upload-time = "2026-08-25T19:44:37.192Z" }, + { url = "https://files.pythonhosted.org/packages/f6/6e/1cf405c5c8e8df7545378048e954792f00b7f2367af8863ce8b8f3e10607/cryptography-50.0.1-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:a255449073358275b64b67d3f595f268bbef70e72b6edb65e0c70c735bf739c9", size = 5332388, upload-time = "2026-08-25T19:44:39.16Z" }, + { url = "https://files.pythonhosted.org/packages/47/92/b4317e8c32c4f47b062f5398bd79106b220a124546f42be83bf32b761e2a/cryptography-50.0.1-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:8df2de9102026855887e4587084f6eabd80ed0f345b8ad8a7ac27ab9bf4723e0", size = 4730293, upload-time = "2026-08-25T19:44:41.298Z" }, + { url = "https://files.pythonhosted.org/packages/39/0d/a1e7633e2c744d0f2983320a27e924ef2264c79c56e1a58d5fb0a1cfd413/cryptography-50.0.1-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:ac02b07824d4d1001bd4367599f839c19cb171924c796e52c23508ac14c2c0cc", size = 4346031, upload-time = "2026-08-25T19:44:43.245Z" }, + { url = "https://files.pythonhosted.org/packages/88/dd/b215616f9bab3fc18510c78a4e5c9f362d77838503c363dc747c7d4f5c6f/cryptography-50.0.1-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:cbf74a81765ee67413503ca6e26dcc4f6f5a519822436cc0a1b97aab6c1b8a17", size = 4715344, upload-time = "2026-08-25T19:44:45.291Z" }, + { url = "https://files.pythonhosted.org/packages/b1/1b/ec3ebd31741d0e963612c4fe43caa39341b9b1e031e469820e42e4c83918/cryptography-50.0.1-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:16c5ecd954b3330ebfb6605eca4fd952da8bef376551d5cc264534e3770a9ee6", size = 5287201, upload-time = "2026-08-25T19:44:47.297Z" }, + { url = "https://files.pythonhosted.org/packages/1a/01/0127d11a762b31a9ee0221894f540318761783f3fdc4bc5d057698caebd5/cryptography-50.0.1-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:79bf008d1f9af6071c797ad133e39915dfee7614f18f18f4db9072eb715064a3", size = 4730023, upload-time = "2026-08-25T19:44:49.435Z" }, + { url = "https://files.pythonhosted.org/packages/9e/b9/e7425ebfb599241a0c1d7000f1b466c3062da66c19d9525031315dff7213/cryptography-50.0.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:330fbb252391c596f1ae42c5754449dc924e6ad012dca8efe0d703f9f2d12ec6", size = 4847362, upload-time = "2026-08-25T19:44:51.94Z" }, + { url = "https://files.pythonhosted.org/packages/2d/fd/60d0ddf4defa12e482c9d5e0f554384d6e8ab25341fd15f060028fd92e6a/cryptography-50.0.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:42be3bb70596b3abe4ac097b75be223e8b3ab614a0e5de068e3dcc54d71d6149", size = 4999247, upload-time = "2026-08-25T19:44:53.876Z" }, + { url = "https://files.pythonhosted.org/packages/4d/56/bc4f2b209e766c93372cfcd59b781a0b2b59700f62a969580415b699c2b2/cryptography-50.0.1-cp314-cp314t-win_amd64.whl", hash = "sha256:f74455bb086a85d5e81246412602aaa97ed095e504cd40dd261ef50be42205bf", size = 3825806, upload-time = "2026-08-25T19:44:56.209Z" }, + { url = "https://files.pythonhosted.org/packages/84/a9/ee16a903f13755e914d1eecc482fe64d1f10761c3960e5d8fa6837377aff/cryptography-50.0.1-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:ca83d00d9e69cd5eb63f2e69c3a5a59e0cecae5ae14c6ae0b35830fe3b37bad0", size = 4035307, upload-time = "2026-08-25T19:44:58.305Z" }, + { url = "https://files.pythonhosted.org/packages/5e/a5/9ec7e81e8526c0d7a387d73386b2daed3f39e10d81a85930bd1b6bfba65c/cryptography-50.0.1-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:05ba322c4da95b262a212c345af888ef2c37c88c0509756ea00a0e6d68850f23", size = 4751900, upload-time = "2026-08-25T19:45:00.401Z" }, + { url = "https://files.pythonhosted.org/packages/7e/3c/0e77bd5ffcf078e9dd27d3074aad6c030d9b10d0bf69329d573c927a188c/cryptography-50.0.1-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e22dfed744bd4002e909464cb23d2f0b05c6f3113a79ef2e9864a53db737c733", size = 4738357, upload-time = "2026-08-25T19:45:02.786Z" }, + { url = "https://files.pythonhosted.org/packages/27/3a/3c5f80daa4dcd47323c7af8a2fcb90de27a33564d4fcac69846c0972691a/cryptography-50.0.1-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:4c4188f7c0cf655be5c06342b817ed0f9595b69ffa2b12026e5353eed29dea88", size = 4758474, upload-time = "2026-08-25T19:45:04.889Z" }, + { url = "https://files.pythonhosted.org/packages/6e/2b/214cf0cf93db9628c3c20c896b229f327f6fb1b20e4b3743d8ad3f00af8b/cryptography-50.0.1-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:2ebbfb0f1fed745e91796e3e1080a1440423fdae8ece1b995a1d80883a409054", size = 5375862, upload-time = "2026-08-25T19:45:07.163Z" }, + { url = "https://files.pythonhosted.org/packages/d6/51/3f9701867a46b6c1740c9b52fc4d3bed6cbdcfedcc9b6e64305c07f39cff/cryptography-50.0.1-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:407fe2b6db00939c05c0e945e9914238f2f0a430974839429dafc82b1ee6bee5", size = 4772942, upload-time = "2026-08-25T19:45:09.396Z" }, + { url = "https://files.pythonhosted.org/packages/0d/5c/13ea642e08e2544d0f5396122055f4820cfacb3203562197b5967125ea97/cryptography-50.0.1-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:2b34d76a652ea2b6faf777c35df230c5637842cd904e04f16230c3f9f03e4361", size = 4383347, upload-time = "2026-08-25T19:45:11.659Z" }, + { url = "https://files.pythonhosted.org/packages/84/d5/7d1fe1cb93f91c428093ff234e128c89ba8ea61a6f26aab406081f9b996e/cryptography-50.0.1-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:01f41478cf33fc605a6a089cd56d28b45c6c0b45a1928b61797f2621a04bac71", size = 4758050, upload-time = "2026-08-25T19:45:13.745Z" }, + { url = "https://files.pythonhosted.org/packages/dd/04/557fc5ead96a829e0bc812a3b9dc4a52a2f27e4f7f5950da7ff27653a805/cryptography-50.0.1-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:fc3ed7ebd2a8c96f5b166de0ab9b624996bef3b07bbeb19364dfb78222c22c80", size = 5332955, upload-time = "2026-08-25T19:45:16.193Z" }, + { url = "https://files.pythonhosted.org/packages/8c/eb/5d7124083e8d8cda8f5b348f544b71ad6f707ad63193758ef4d8e569da02/cryptography-50.0.1-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:9dde0a357190eb3b1da1bb9ab750e9c85cba82ca5977aa0836cbb94e92611239", size = 4772694, upload-time = "2026-08-25T19:45:18.315Z" }, + { url = "https://files.pythonhosted.org/packages/63/8e/f1f955e0921dd2b6d22eae7e8d24a4c4b638d10735ffbf6a71f99eb0fcb8/cryptography-50.0.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:fd3718b960d0b5dd213cdf03f3bcb7000e69dda0de8b956061947ff6bcff5558", size = 4888413, upload-time = "2026-08-25T19:45:20.4Z" }, + { url = "https://files.pythonhosted.org/packages/1f/ab/89e2b798d2c3925f82e2bb72d5979f3d2f6da2dd22ef4a8cd8b70d920039/cryptography-50.0.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:2a93d05e34d5f67fba6f891fe85d929999baa7195e853923ea6d7576c9e68c5e", size = 5044355, upload-time = "2026-08-25T19:45:22.353Z" }, + { url = "https://files.pythonhosted.org/packages/99/89/87ef49ffe383ef4e147d27b7bf2088fb0b54ea409dd87b5a89442e5828a5/cryptography-50.0.1-cp39-abi3-win_amd64.whl", hash = "sha256:55d16b1ef3ee0958d893a977b19777887e546c9954ea81b200c3301a864013f2", size = 3875429, upload-time = "2026-08-25T19:45:24.418Z" }, + { url = "https://files.pythonhosted.org/packages/c7/27/8d207af749c453ee17ea087340b3f2b4adef75aadd1d277b1b129bdda84e/cryptography-50.0.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:9cb3cb952cf5a8abd50c782a98a89d71699715e802fe349704b47f2425b42a94", size = 3974350, upload-time = "2026-08-25T19:45:26.551Z" }, + { url = "https://files.pythonhosted.org/packages/14/9a/6d3a4d7852e22d657438b7bf51f66102c7d71c0e1fafeec652281d0403e5/cryptography-50.0.1-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:5fe939deeb161024a6be98229c953b6591fef1f41214497a78fe793a244c017f", size = 4698675, upload-time = "2026-08-25T19:45:28.658Z" }, + { url = "https://files.pythonhosted.org/packages/73/35/5c3717edf9e68a0550ce04e28eab493fe545eccd81742af03f6a75fe260b/cryptography-50.0.1-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:fb4b9672d389c738b175c4166e78310f8a70358886aacd9173ee03a85ffdc671", size = 4707410, upload-time = "2026-08-25T19:45:30.816Z" }, + { url = "https://files.pythonhosted.org/packages/1d/e0/e786934472e3ac4ecdecc7b129a0ca1a2a40dffdafcf2c3ea9d4397f8def/cryptography-50.0.1-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:d63ae8f6481fec907ac0f588eee8a90aefde112c633131fe540e5711ddbb5a4e", size = 4698378, upload-time = "2026-08-25T19:45:33.043Z" }, + { url = "https://files.pythonhosted.org/packages/51/cf/5b3f53a0b74d122f023476ede40ba5d3e70d5cf475f73b899740d26a4fb2/cryptography-50.0.1-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:804728ce710890870f3aaa344b2e161172d258d768ac139d02cfd9092d0d94e6", size = 4706889, upload-time = "2026-08-25T19:45:35.086Z" }, + { url = "https://files.pythonhosted.org/packages/71/44/711e61f7d014be825ef79b285b047292d1bf893732ac1bc030a351fb517f/cryptography-50.0.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:693c99b49bd37d0d096e4334c10232c77248c415b98d35236094cdf96d57258b", size = 3824006, upload-time = "2026-08-25T19:45:37.281Z" }, +] + +[[package]] +name = "duckdb" +version = "1.5.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/19/e57151753576373c6696a12022648546cca6038e8833fda2908ee2342d9b/duckdb-1.5.5.tar.gz", hash = "sha256:72f33ee57ca7595b23957671a2cc7f7fe2be0ecc2d68f63abedcfcaa3a5c1238", size = 18066741, upload-time = "2026-07-22T10:55:17.819Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9c/c2/b62ec24d57bb8df4e24b0b58f7f8facb32f5fdb9f1895aed9e9fcdded168/duckdb-1.5.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:1b543841b0ae18a9c982345cfa3987e9c065d3a4b0f067daa473d92d1e65f528", size = 32708371, upload-time = "2026-07-22T10:53:41.642Z" }, + { url = "https://files.pythonhosted.org/packages/8a/ce/769171ba45f0b73632dc3bc3108d891e81dd6c6bbfba630a34a75b4dcc0f/duckdb-1.5.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1a925d06c2a4c3b64553d6cc1aced5028d376d4479bed689a7d47e9b1dccd80a", size = 17343979, upload-time = "2026-07-22T10:53:44.951Z" }, + { url = "https://files.pythonhosted.org/packages/46/59/a8e3384ee916e00d5dcf985194c1511d61978540778a1e96fa47f9fb3e0d/duckdb-1.5.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0c42757cb34722144bd4dfb94b6f336339e7b2468f6813fa7fa9a319ba07bab4", size = 15493704, upload-time = "2026-07-22T10:53:47.912Z" }, + { url = "https://files.pythonhosted.org/packages/6f/1d/9840179c2607b90523a2884a129c4d4e6dbdc1178ba62a976c1043beba88/duckdb-1.5.5-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2e72f9e1a4f90a5c8483ad4d540e495bf0834ba61c360b52499a573d7ed62a3f", size = 19366574, upload-time = "2026-07-22T10:53:51.876Z" }, + { url = "https://files.pythonhosted.org/packages/b5/55/f9641a4eebcc2f4df631287d6c3b9ed2eea3b92644f93acbad825e3972b6/duckdb-1.5.5-cp311-cp311-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b9b6f86ed85d4ef5e0211eaebf75d057bd8bb520bba438a95dd0f4e42234bbfe", size = 21477952, upload-time = "2026-07-22T10:53:55.575Z" }, + { url = "https://files.pythonhosted.org/packages/3c/3a/07c3556e37a5c97b95917b029c8fdde4a25fbd76a660bacdac195cf20dcb/duckdb-1.5.5-cp311-cp311-win_amd64.whl", hash = "sha256:9f4287f97ccf0c1f3d471e7115be2b067cbf99627e2d34bffd462dd64703cddc", size = 13156986, upload-time = "2026-07-22T10:53:58.823Z" }, + { url = "https://files.pythonhosted.org/packages/4f/ff/07b48eef2078ca033847e9caa46cc7633b714c5f91ad1ce091c8ca89d792/duckdb-1.5.5-cp311-cp311-win_arm64.whl", hash = "sha256:179633a3fc6296c75d57c69c1e239fa9e5cdcb670fd1dbff88a02663f932905c", size = 14001317, upload-time = "2026-07-22T10:54:01.724Z" }, + { url = "https://files.pythonhosted.org/packages/d6/40/2e05d324400fdaa5656c9f48d6298da421cb034d85e509fa0e6e325cf04b/duckdb-1.5.5-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d4dd65f8941a604b947e0b9b4b4f7165988e29a23ec0b69b4038520956d9933e", size = 32753858, upload-time = "2026-07-22T10:54:05.514Z" }, + { url = "https://files.pythonhosted.org/packages/79/15/5ceb58ffb5bb8a62b3fd7abb39c41467cdf94850ece02e6d88664dfc75ce/duckdb-1.5.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:33db46679b071f108d57139493dee2d37e1f5efcf5c5c039c2969eed11a6c8a7", size = 17368293, upload-time = "2026-07-22T10:54:09.139Z" }, + { url = "https://files.pythonhosted.org/packages/bf/5c/bf02da0b354fe83cca4f95a4fbf762181af466f7d551ab2a093f7698882a/duckdb-1.5.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f0b88535a5d86fdd63dba6ea02ab68c003dfb9e4892b11256ef24c4da208baae", size = 15509131, upload-time = "2026-07-22T10:54:12.228Z" }, + { url = "https://files.pythonhosted.org/packages/ea/a9/5f1f09da421d8e930e0b063d11c1b3f90363f40ede74438cd188afdd13a2/duckdb-1.5.5-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f316eae2323d9a851883fdf2dee91c1f9efe251ab33e14a2272f82a913422ed6", size = 19391959, upload-time = "2026-07-22T10:54:15.551Z" }, + { url = "https://files.pythonhosted.org/packages/4f/98/6549769f158126fa64fd6c1ac2eb59a18282146c939867a3eb31b7c1db07/duckdb-1.5.5-cp312-cp312-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7a6d2d11859d82a936ebdcb30ce3d8a1cbb3e990bff05c12abb9b54c44fa7bd1", size = 21510909, upload-time = "2026-07-22T10:54:19.681Z" }, + { url = "https://files.pythonhosted.org/packages/af/b7/5753b41d3124838f868f9f523362812d9fc45409e9e4dd70dcbb0a25826e/duckdb-1.5.5-cp312-cp312-win_amd64.whl", hash = "sha256:ddfbdb096c11d51ee22492397d342c90a82e62c5d09961477895934d0a25372f", size = 13168544, upload-time = "2026-07-22T10:54:22.789Z" }, + { url = "https://files.pythonhosted.org/packages/5c/28/44b679c7d46245f8398feae7edac959d1b83d4eb143e25b3fce0630b78bd/duckdb-1.5.5-cp312-cp312-win_arm64.whl", hash = "sha256:2725d2b9ace3a4e75d72fc5a239f6a44b502c580edadb8fb2676db772c5f9282", size = 13988684, upload-time = "2026-07-22T10:54:26.003Z" }, + { url = "https://files.pythonhosted.org/packages/47/37/4a38116e7700720fd152c666292214fd3abdf916496991296d8d1f66efbf/duckdb-1.5.5-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:cd98829b67788609017e65c761bd42a5dd0f9129441bed8bda4d6881ccf819f0", size = 32754294, upload-time = "2026-07-22T10:54:29.822Z" }, + { url = "https://files.pythonhosted.org/packages/66/42/7d392f1ba1eee0eaf4ab4c8c7a604bfe3536cd63f979cf5c98798664f807/duckdb-1.5.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:feead93c56679b79592d437c62975d39cb67adedffa7592c763baf8160ac7366", size = 17368211, upload-time = "2026-07-22T10:54:33.359Z" }, + { url = "https://files.pythonhosted.org/packages/9f/a5/0a6f4fa60562faa615e55e15bd1953a2f2b17a8edd8105e5cda215e43457/duckdb-1.5.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:49c963d9469373d7aba8d750d9ea565ab823e94166efed953f184dd9b169b98c", size = 15509136, upload-time = "2026-07-22T10:54:36.369Z" }, + { url = "https://files.pythonhosted.org/packages/e4/cb/023c89f51978545b9fab318581bba0c457a58e7530d2d933e54ae7d8647c/duckdb-1.5.5-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a736217825461732b5442d05a220f3da2e23a0dae114efbf08c9bf171b53098a", size = 19392147, upload-time = "2026-07-22T10:54:39.551Z" }, + { url = "https://files.pythonhosted.org/packages/3e/c5/41bef391fb8b23dbc133c9f2ba016e7a7a8124513d2cc1b430f1897d87e4/duckdb-1.5.5-cp313-cp313-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:078e6a60dd8eedde5832f45422ca5c4a6b8c837aeabd8a56ca0b7d933f588053", size = 21511060, upload-time = "2026-07-22T10:54:42.788Z" }, + { url = "https://files.pythonhosted.org/packages/07/9f/c44dfc1f924ac29b3252dc1b91393c01d009dbfe9f8ed33f10b986151bd1/duckdb-1.5.5-cp313-cp313-win_amd64.whl", hash = "sha256:6826504277dba513c0c5d71d828456c94d729c9d2482f94b2e289f90a9167e28", size = 13168028, upload-time = "2026-07-22T10:54:46.127Z" }, + { url = "https://files.pythonhosted.org/packages/ca/88/591384b2cd59abddd6f5dc175e60374f9abae6064429f0c4402854c10f44/duckdb-1.5.5-cp313-cp313-win_arm64.whl", hash = "sha256:baa9c5702002fabb559ded2a39008f9f421fcbc7237d388b8213eff1e08858de", size = 13989955, upload-time = "2026-07-22T10:54:49.262Z" }, + { url = "https://files.pythonhosted.org/packages/3e/56/12c65bfa2d2605b81981b264788891bcf11ec72227889554cead5d8d13b9/duckdb-1.5.5-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8e6413dd40facb7b8ab21bd844450cd8f549b29e138635be9cf090ef4d2049e2", size = 32761946, upload-time = "2026-07-22T10:54:53.412Z" }, + { url = "https://files.pythonhosted.org/packages/b9/46/682ce155f17e0d2822d4f13ee3db9ca4b5b7c2da61b841b2629035e1f4bc/duckdb-1.5.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:64078acfd16541132ac6e191eb81b2845554444a0305cc1aa581ba107e514aa8", size = 17375069, upload-time = "2026-07-22T10:54:57.269Z" }, + { url = "https://files.pythonhosted.org/packages/39/ce/a24bcbd3289c8f305a430759c5fc12242740b4af3e17f7593f3a34e333d2/duckdb-1.5.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8c11775cc99a447618d5f1840126db17f2652f3eae05529df4f81f40e2df7151", size = 15519791, upload-time = "2026-07-22T10:55:00.681Z" }, + { url = "https://files.pythonhosted.org/packages/d9/76/3a01afbc615c1d418c0de58a6b68ac5ce2a8563232c0464bfbc2ce552398/duckdb-1.5.5-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:77bbc1e6ba12e1e06f9020117bdf848627ecfdf36f907550e62e008e6109dece", size = 19398251, upload-time = "2026-07-22T10:55:04.168Z" }, + { url = "https://files.pythonhosted.org/packages/a1/43/3a5e81d1728f4d234c79bfe385808ee7c04834f7c37a4b5c257459c25614/duckdb-1.5.5-cp314-cp314-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fbf0f2d48b43c6c304d00463b463c27ead6c4b01c3c1816b750f728decf71afe", size = 21513851, upload-time = "2026-07-22T10:55:07.864Z" }, + { url = "https://files.pythonhosted.org/packages/91/41/fc7c829172c60ca22485251eab285f4f1a0d87b486a024c726f21471d86e/duckdb-1.5.5-cp314-cp314-win_amd64.whl", hash = "sha256:9dc826c4b50e64f6c4e4d07a3a9cb075ef70ba3899dc43ec5493dc3d7b04b353", size = 13691858, upload-time = "2026-07-22T10:55:11.181Z" }, + { url = "https://files.pythonhosted.org/packages/e1/2c/95d9216b79e9273689d7ebce125a54503ed0c9bd7da931f0265888e99779/duckdb-1.5.5-cp314-cp314-win_arm64.whl", hash = "sha256:63e48d4b74b15aeacd688976432a7225163df8c226eddeb8536bba2d4d4ff433", size = 14470180, upload-time = "2026-07-22T10:55:14.445Z" }, +] + +[[package]] +name = "fastapi" +version = "0.141.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-doc" }, + { name = "pydantic" }, + { name = "starlette" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8a/02/91e3416a8fdd715abb903a952a6bec7cdd8d14eed55d415fc8595524c319/fastapi-0.141.1.tar.gz", hash = "sha256:e8822fc40db1e1858054d7a949a888695bc9bdce70139178e33bd2871a453ca1", size = 425799, upload-time = "2026-07-29T17:18:05.568Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/03/10388a42375ee7e4ac9b94eb2c5c569c8b5795e377e701c9ac3ad63de890/fastapi-0.141.1-py3-none-any.whl", hash = "sha256:bfb91aa2d334c61cb35ba9a116fc123b3d3df31640b801cf57a7a78ec3f603b3", size = 131954, upload-time = "2026-07-29T17:18:04.364Z" }, +] + +[[package]] +name = "filelock" +version = "3.32.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0f/59/e19834834cb01a32febfbb0f8a23a9088088f5d45991824ff2bc3b5e8acb/filelock-3.32.7.tar.gz", hash = "sha256:37b8a3d9811b0f9aef7e5ec5c71bb320de52df51e6ca9bcd6f5ad81187660da7", size = 225154, upload-time = "2026-09-16T00:24:20.907Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/15/df/31098c5aeb4d966b553641472bd55fcf5fdfac953549894b8a765ba44e91/filelock-3.32.7-py3-none-any.whl", hash = "sha256:65ff0d0190ea42038b32bda4b77834fb05be2cad4c5b9b01aa4dfb3614536e52", size = 100157, upload-time = "2026-09-16T00:24:19.543Z" }, +] + +[[package]] +name = "frozenlist" +version = "1.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2d/f5/c831fac6cc817d26fd54c7eaccd04ef7e0288806943f7cc5bbf69f3ac1f0/frozenlist-1.8.0.tar.gz", hash = "sha256:3ede829ed8d842f6cd48fc7081d7a41001a56f1f38603f9d49bf3020d59a31ad", size = 45875, upload-time = "2025-10-06T05:38:17.865Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bc/03/077f869d540370db12165c0aa51640a873fb661d8b315d1d4d67b284d7ac/frozenlist-1.8.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:09474e9831bc2b2199fad6da3c14c7b0fbdd377cce9d3d77131be28906cb7d84", size = 86912, upload-time = "2025-10-06T05:35:45.98Z" }, + { url = "https://files.pythonhosted.org/packages/df/b5/7610b6bd13e4ae77b96ba85abea1c8cb249683217ef09ac9e0ae93f25a91/frozenlist-1.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:17c883ab0ab67200b5f964d2b9ed6b00971917d5d8a92df149dc2c9779208ee9", size = 50046, upload-time = "2025-10-06T05:35:47.009Z" }, + { url = "https://files.pythonhosted.org/packages/6e/ef/0e8f1fe32f8a53dd26bdd1f9347efe0778b0fddf62789ea683f4cc7d787d/frozenlist-1.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fa47e444b8ba08fffd1c18e8cdb9a75db1b6a27f17507522834ad13ed5922b93", size = 50119, upload-time = "2025-10-06T05:35:48.38Z" }, + { url = "https://files.pythonhosted.org/packages/11/b1/71a477adc7c36e5fb628245dfbdea2166feae310757dea848d02bd0689fd/frozenlist-1.8.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2552f44204b744fba866e573be4c1f9048d6a324dfe14475103fd51613eb1d1f", size = 231067, upload-time = "2025-10-06T05:35:49.97Z" }, + { url = "https://files.pythonhosted.org/packages/45/7e/afe40eca3a2dc19b9904c0f5d7edfe82b5304cb831391edec0ac04af94c2/frozenlist-1.8.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:957e7c38f250991e48a9a73e6423db1bb9dd14e722a10f6b8bb8e16a0f55f695", size = 233160, upload-time = "2025-10-06T05:35:51.729Z" }, + { url = "https://files.pythonhosted.org/packages/a6/aa/7416eac95603ce428679d273255ffc7c998d4132cfae200103f164b108aa/frozenlist-1.8.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8585e3bb2cdea02fc88ffa245069c36555557ad3609e83be0ec71f54fd4abb52", size = 228544, upload-time = "2025-10-06T05:35:53.246Z" }, + { url = "https://files.pythonhosted.org/packages/8b/3d/2a2d1f683d55ac7e3875e4263d28410063e738384d3adc294f5ff3d7105e/frozenlist-1.8.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:edee74874ce20a373d62dc28b0b18b93f645633c2943fd90ee9d898550770581", size = 243797, upload-time = "2025-10-06T05:35:54.497Z" }, + { url = "https://files.pythonhosted.org/packages/78/1e/2d5565b589e580c296d3bb54da08d206e797d941a83a6fdea42af23be79c/frozenlist-1.8.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c9a63152fe95756b85f31186bddf42e4c02c6321207fd6601a1c89ebac4fe567", size = 247923, upload-time = "2025-10-06T05:35:55.861Z" }, + { url = "https://files.pythonhosted.org/packages/aa/c3/65872fcf1d326a7f101ad4d86285c403c87be7d832b7470b77f6d2ed5ddc/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b6db2185db9be0a04fecf2f241c70b63b1a242e2805be291855078f2b404dd6b", size = 230886, upload-time = "2025-10-06T05:35:57.399Z" }, + { url = "https://files.pythonhosted.org/packages/a0/76/ac9ced601d62f6956f03cc794f9e04c81719509f85255abf96e2510f4265/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:f4be2e3d8bc8aabd566f8d5b8ba7ecc09249d74ba3c9ed52e54dc23a293f0b92", size = 245731, upload-time = "2025-10-06T05:35:58.563Z" }, + { url = "https://files.pythonhosted.org/packages/b9/49/ecccb5f2598daf0b4a1415497eba4c33c1e8ce07495eb07d2860c731b8d5/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:c8d1634419f39ea6f5c427ea2f90ca85126b54b50837f31497f3bf38266e853d", size = 241544, upload-time = "2025-10-06T05:35:59.719Z" }, + { url = "https://files.pythonhosted.org/packages/53/4b/ddf24113323c0bbcc54cb38c8b8916f1da7165e07b8e24a717b4a12cbf10/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:1a7fa382a4a223773ed64242dbe1c9c326ec09457e6b8428efb4118c685c3dfd", size = 241806, upload-time = "2025-10-06T05:36:00.959Z" }, + { url = "https://files.pythonhosted.org/packages/a7/fb/9b9a084d73c67175484ba2789a59f8eebebd0827d186a8102005ce41e1ba/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:11847b53d722050808926e785df837353bd4d75f1d494377e59b23594d834967", size = 229382, upload-time = "2025-10-06T05:36:02.22Z" }, + { url = "https://files.pythonhosted.org/packages/95/a3/c8fb25aac55bf5e12dae5c5aa6a98f85d436c1dc658f21c3ac73f9fa95e5/frozenlist-1.8.0-cp311-cp311-win32.whl", hash = "sha256:27c6e8077956cf73eadd514be8fb04d77fc946a7fe9f7fe167648b0b9085cc25", size = 39647, upload-time = "2025-10-06T05:36:03.409Z" }, + { url = "https://files.pythonhosted.org/packages/0a/f5/603d0d6a02cfd4c8f2a095a54672b3cf967ad688a60fb9faf04fc4887f65/frozenlist-1.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:ac913f8403b36a2c8610bbfd25b8013488533e71e62b4b4adce9c86c8cea905b", size = 44064, upload-time = "2025-10-06T05:36:04.368Z" }, + { url = "https://files.pythonhosted.org/packages/5d/16/c2c9ab44e181f043a86f9a8f84d5124b62dbcb3a02c0977ec72b9ac1d3e0/frozenlist-1.8.0-cp311-cp311-win_arm64.whl", hash = "sha256:d4d3214a0f8394edfa3e303136d0575eece0745ff2b47bd2cb2e66dd92d4351a", size = 39937, upload-time = "2025-10-06T05:36:05.669Z" }, + { url = "https://files.pythonhosted.org/packages/69/29/948b9aa87e75820a38650af445d2ef2b6b8a6fab1a23b6bb9e4ef0be2d59/frozenlist-1.8.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:78f7b9e5d6f2fdb88cdde9440dc147259b62b9d3b019924def9f6478be254ac1", size = 87782, upload-time = "2025-10-06T05:36:06.649Z" }, + { url = "https://files.pythonhosted.org/packages/64/80/4f6e318ee2a7c0750ed724fa33a4bdf1eacdc5a39a7a24e818a773cd91af/frozenlist-1.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:229bf37d2e4acdaf808fd3f06e854a4a7a3661e871b10dc1f8f1896a3b05f18b", size = 50594, upload-time = "2025-10-06T05:36:07.69Z" }, + { url = "https://files.pythonhosted.org/packages/2b/94/5c8a2b50a496b11dd519f4a24cb5496cf125681dd99e94c604ccdea9419a/frozenlist-1.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f833670942247a14eafbb675458b4e61c82e002a148f49e68257b79296e865c4", size = 50448, upload-time = "2025-10-06T05:36:08.78Z" }, + { url = "https://files.pythonhosted.org/packages/6a/bd/d91c5e39f490a49df14320f4e8c80161cfcce09f1e2cde1edd16a551abb3/frozenlist-1.8.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:494a5952b1c597ba44e0e78113a7266e656b9794eec897b19ead706bd7074383", size = 242411, upload-time = "2025-10-06T05:36:09.801Z" }, + { url = "https://files.pythonhosted.org/packages/8f/83/f61505a05109ef3293dfb1ff594d13d64a2324ac3482be2cedc2be818256/frozenlist-1.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96f423a119f4777a4a056b66ce11527366a8bb92f54e541ade21f2374433f6d4", size = 243014, upload-time = "2025-10-06T05:36:11.394Z" }, + { url = "https://files.pythonhosted.org/packages/d8/cb/cb6c7b0f7d4023ddda30cf56b8b17494eb3a79e3fda666bf735f63118b35/frozenlist-1.8.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3462dd9475af2025c31cc61be6652dfa25cbfb56cbbf52f4ccfe029f38decaf8", size = 234909, upload-time = "2025-10-06T05:36:12.598Z" }, + { url = "https://files.pythonhosted.org/packages/31/c5/cd7a1f3b8b34af009fb17d4123c5a778b44ae2804e3ad6b86204255f9ec5/frozenlist-1.8.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c4c800524c9cd9bac5166cd6f55285957fcfc907db323e193f2afcd4d9abd69b", size = 250049, upload-time = "2025-10-06T05:36:14.065Z" }, + { url = "https://files.pythonhosted.org/packages/c0/01/2f95d3b416c584a1e7f0e1d6d31998c4a795f7544069ee2e0962a4b60740/frozenlist-1.8.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d6a5df73acd3399d893dafc71663ad22534b5aa4f94e8a2fabfe856c3c1b6a52", size = 256485, upload-time = "2025-10-06T05:36:15.39Z" }, + { url = "https://files.pythonhosted.org/packages/ce/03/024bf7720b3abaebcff6d0793d73c154237b85bdf67b7ed55e5e9596dc9a/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:405e8fe955c2280ce66428b3ca55e12b3c4e9c336fb2103a4937e891c69a4a29", size = 237619, upload-time = "2025-10-06T05:36:16.558Z" }, + { url = "https://files.pythonhosted.org/packages/69/fa/f8abdfe7d76b731f5d8bd217827cf6764d4f1d9763407e42717b4bed50a0/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:908bd3f6439f2fef9e85031b59fd4f1297af54415fb60e4254a95f75b3cab3f3", size = 250320, upload-time = "2025-10-06T05:36:17.821Z" }, + { url = "https://files.pythonhosted.org/packages/f5/3c/b051329f718b463b22613e269ad72138cc256c540f78a6de89452803a47d/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:294e487f9ec720bd8ffcebc99d575f7eff3568a08a253d1ee1a0378754b74143", size = 246820, upload-time = "2025-10-06T05:36:19.046Z" }, + { url = "https://files.pythonhosted.org/packages/0f/ae/58282e8f98e444b3f4dd42448ff36fa38bef29e40d40f330b22e7108f565/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:74c51543498289c0c43656701be6b077f4b265868fa7f8a8859c197006efb608", size = 250518, upload-time = "2025-10-06T05:36:20.763Z" }, + { url = "https://files.pythonhosted.org/packages/8f/96/007e5944694d66123183845a106547a15944fbbb7154788cbf7272789536/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:776f352e8329135506a1d6bf16ac3f87bc25b28e765949282dcc627af36123aa", size = 239096, upload-time = "2025-10-06T05:36:22.129Z" }, + { url = "https://files.pythonhosted.org/packages/66/bb/852b9d6db2fa40be96f29c0d1205c306288f0684df8fd26ca1951d461a56/frozenlist-1.8.0-cp312-cp312-win32.whl", hash = "sha256:433403ae80709741ce34038da08511d4a77062aa924baf411ef73d1146e74faf", size = 39985, upload-time = "2025-10-06T05:36:23.661Z" }, + { url = "https://files.pythonhosted.org/packages/b8/af/38e51a553dd66eb064cdf193841f16f077585d4d28394c2fa6235cb41765/frozenlist-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:34187385b08f866104f0c0617404c8eb08165ab1272e884abc89c112e9c00746", size = 44591, upload-time = "2025-10-06T05:36:24.958Z" }, + { url = "https://files.pythonhosted.org/packages/a7/06/1dc65480ab147339fecc70797e9c2f69d9cea9cf38934ce08df070fdb9cb/frozenlist-1.8.0-cp312-cp312-win_arm64.whl", hash = "sha256:fe3c58d2f5db5fbd18c2987cba06d51b0529f52bc3a6cdc33d3f4eab725104bd", size = 40102, upload-time = "2025-10-06T05:36:26.333Z" }, + { url = "https://files.pythonhosted.org/packages/2d/40/0832c31a37d60f60ed79e9dfb5a92e1e2af4f40a16a29abcc7992af9edff/frozenlist-1.8.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8d92f1a84bb12d9e56f818b3a746f3efba93c1b63c8387a73dde655e1e42282a", size = 85717, upload-time = "2025-10-06T05:36:27.341Z" }, + { url = "https://files.pythonhosted.org/packages/30/ba/b0b3de23f40bc55a7057bd38434e25c34fa48e17f20ee273bbde5e0650f3/frozenlist-1.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:96153e77a591c8adc2ee805756c61f59fef4cf4073a9275ee86fe8cba41241f7", size = 49651, upload-time = "2025-10-06T05:36:28.855Z" }, + { url = "https://files.pythonhosted.org/packages/0c/ab/6e5080ee374f875296c4243c381bbdef97a9ac39c6e3ce1d5f7d42cb78d6/frozenlist-1.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f21f00a91358803399890ab167098c131ec2ddd5f8f5fd5fe9c9f2c6fcd91e40", size = 49417, upload-time = "2025-10-06T05:36:29.877Z" }, + { url = "https://files.pythonhosted.org/packages/d5/4e/e4691508f9477ce67da2015d8c00acd751e6287739123113a9fca6f1604e/frozenlist-1.8.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fb30f9626572a76dfe4293c7194a09fb1fe93ba94c7d4f720dfae3b646b45027", size = 234391, upload-time = "2025-10-06T05:36:31.301Z" }, + { url = "https://files.pythonhosted.org/packages/40/76/c202df58e3acdf12969a7895fd6f3bc016c642e6726aa63bd3025e0fc71c/frozenlist-1.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eaa352d7047a31d87dafcacbabe89df0aa506abb5b1b85a2fb91bc3faa02d822", size = 233048, upload-time = "2025-10-06T05:36:32.531Z" }, + { url = "https://files.pythonhosted.org/packages/f9/c0/8746afb90f17b73ca5979c7a3958116e105ff796e718575175319b5bb4ce/frozenlist-1.8.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:03ae967b4e297f58f8c774c7eabcce57fe3c2434817d4385c50661845a058121", size = 226549, upload-time = "2025-10-06T05:36:33.706Z" }, + { url = "https://files.pythonhosted.org/packages/7e/eb/4c7eefc718ff72f9b6c4893291abaae5fbc0c82226a32dcd8ef4f7a5dbef/frozenlist-1.8.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f6292f1de555ffcc675941d65fffffb0a5bcd992905015f85d0592201793e0e5", size = 239833, upload-time = "2025-10-06T05:36:34.947Z" }, + { url = "https://files.pythonhosted.org/packages/c2/4e/e5c02187cf704224f8b21bee886f3d713ca379535f16893233b9d672ea71/frozenlist-1.8.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29548f9b5b5e3460ce7378144c3010363d8035cea44bc0bf02d57f5a685e084e", size = 245363, upload-time = "2025-10-06T05:36:36.534Z" }, + { url = "https://files.pythonhosted.org/packages/1f/96/cb85ec608464472e82ad37a17f844889c36100eed57bea094518bf270692/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ec3cc8c5d4084591b4237c0a272cc4f50a5b03396a47d9caaf76f5d7b38a4f11", size = 229314, upload-time = "2025-10-06T05:36:38.582Z" }, + { url = "https://files.pythonhosted.org/packages/5d/6f/4ae69c550e4cee66b57887daeebe006fe985917c01d0fff9caab9883f6d0/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:517279f58009d0b1f2e7c1b130b377a349405da3f7621ed6bfae50b10adf20c1", size = 243365, upload-time = "2025-10-06T05:36:40.152Z" }, + { url = "https://files.pythonhosted.org/packages/7a/58/afd56de246cf11780a40a2c28dc7cbabbf06337cc8ddb1c780a2d97e88d8/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:db1e72ede2d0d7ccb213f218df6a078a9c09a7de257c2fe8fcef16d5925230b1", size = 237763, upload-time = "2025-10-06T05:36:41.355Z" }, + { url = "https://files.pythonhosted.org/packages/cb/36/cdfaf6ed42e2644740d4a10452d8e97fa1c062e2a8006e4b09f1b5fd7d63/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b4dec9482a65c54a5044486847b8a66bf10c9cb4926d42927ec4e8fd5db7fed8", size = 240110, upload-time = "2025-10-06T05:36:42.716Z" }, + { url = "https://files.pythonhosted.org/packages/03/a8/9ea226fbefad669f11b52e864c55f0bd57d3c8d7eb07e9f2e9a0b39502e1/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:21900c48ae04d13d416f0e1e0c4d81f7931f73a9dfa0b7a8746fb2fe7dd970ed", size = 233717, upload-time = "2025-10-06T05:36:44.251Z" }, + { url = "https://files.pythonhosted.org/packages/1e/0b/1b5531611e83ba7d13ccc9988967ea1b51186af64c42b7a7af465dcc9568/frozenlist-1.8.0-cp313-cp313-win32.whl", hash = "sha256:8b7b94a067d1c504ee0b16def57ad5738701e4ba10cec90529f13fa03c833496", size = 39628, upload-time = "2025-10-06T05:36:45.423Z" }, + { url = "https://files.pythonhosted.org/packages/d8/cf/174c91dbc9cc49bc7b7aab74d8b734e974d1faa8f191c74af9b7e80848e6/frozenlist-1.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:878be833caa6a3821caf85eb39c5ba92d28e85df26d57afb06b35b2efd937231", size = 43882, upload-time = "2025-10-06T05:36:46.796Z" }, + { url = "https://files.pythonhosted.org/packages/c1/17/502cd212cbfa96eb1388614fe39a3fc9ab87dbbe042b66f97acb57474834/frozenlist-1.8.0-cp313-cp313-win_arm64.whl", hash = "sha256:44389d135b3ff43ba8cc89ff7f51f5a0bb6b63d829c8300f79a2fe4fe61bcc62", size = 39676, upload-time = "2025-10-06T05:36:47.8Z" }, + { url = "https://files.pythonhosted.org/packages/d2/5c/3bbfaa920dfab09e76946a5d2833a7cbdf7b9b4a91c714666ac4855b88b4/frozenlist-1.8.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:e25ac20a2ef37e91c1b39938b591457666a0fa835c7783c3a8f33ea42870db94", size = 89235, upload-time = "2025-10-06T05:36:48.78Z" }, + { url = "https://files.pythonhosted.org/packages/d2/d6/f03961ef72166cec1687e84e8925838442b615bd0b8854b54923ce5b7b8a/frozenlist-1.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:07cdca25a91a4386d2e76ad992916a85038a9b97561bf7a3fd12d5d9ce31870c", size = 50742, upload-time = "2025-10-06T05:36:49.837Z" }, + { url = "https://files.pythonhosted.org/packages/1e/bb/a6d12b7ba4c3337667d0e421f7181c82dda448ce4e7ad7ecd249a16fa806/frozenlist-1.8.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4e0c11f2cc6717e0a741f84a527c52616140741cd812a50422f83dc31749fb52", size = 51725, upload-time = "2025-10-06T05:36:50.851Z" }, + { url = "https://files.pythonhosted.org/packages/bc/71/d1fed0ffe2c2ccd70b43714c6cab0f4188f09f8a67a7914a6b46ee30f274/frozenlist-1.8.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b3210649ee28062ea6099cfda39e147fa1bc039583c8ee4481cb7811e2448c51", size = 284533, upload-time = "2025-10-06T05:36:51.898Z" }, + { url = "https://files.pythonhosted.org/packages/c9/1f/fb1685a7b009d89f9bf78a42d94461bc06581f6e718c39344754a5d9bada/frozenlist-1.8.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:581ef5194c48035a7de2aefc72ac6539823bb71508189e5de01d60c9dcd5fa65", size = 292506, upload-time = "2025-10-06T05:36:53.101Z" }, + { url = "https://files.pythonhosted.org/packages/e6/3b/b991fe1612703f7e0d05c0cf734c1b77aaf7c7d321df4572e8d36e7048c8/frozenlist-1.8.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3ef2d026f16a2b1866e1d86fc4e1291e1ed8a387b2c333809419a2f8b3a77b82", size = 274161, upload-time = "2025-10-06T05:36:54.309Z" }, + { url = "https://files.pythonhosted.org/packages/ca/ec/c5c618767bcdf66e88945ec0157d7f6c4a1322f1473392319b7a2501ded7/frozenlist-1.8.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5500ef82073f599ac84d888e3a8c1f77ac831183244bfd7f11eaa0289fb30714", size = 294676, upload-time = "2025-10-06T05:36:55.566Z" }, + { url = "https://files.pythonhosted.org/packages/7c/ce/3934758637d8f8a88d11f0585d6495ef54b2044ed6ec84492a91fa3b27aa/frozenlist-1.8.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:50066c3997d0091c411a66e710f4e11752251e6d2d73d70d8d5d4c76442a199d", size = 300638, upload-time = "2025-10-06T05:36:56.758Z" }, + { url = "https://files.pythonhosted.org/packages/fc/4f/a7e4d0d467298f42de4b41cbc7ddaf19d3cfeabaf9ff97c20c6c7ee409f9/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5c1c8e78426e59b3f8005e9b19f6ff46e5845895adbde20ece9218319eca6506", size = 283067, upload-time = "2025-10-06T05:36:57.965Z" }, + { url = "https://files.pythonhosted.org/packages/dc/48/c7b163063d55a83772b268e6d1affb960771b0e203b632cfe09522d67ea5/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:eefdba20de0d938cec6a89bd4d70f346a03108a19b9df4248d3cf0d88f1b0f51", size = 292101, upload-time = "2025-10-06T05:36:59.237Z" }, + { url = "https://files.pythonhosted.org/packages/9f/d0/2366d3c4ecdc2fd391e0afa6e11500bfba0ea772764d631bbf82f0136c9d/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:cf253e0e1c3ceb4aaff6df637ce033ff6535fb8c70a764a8f46aafd3d6ab798e", size = 289901, upload-time = "2025-10-06T05:37:00.811Z" }, + { url = "https://files.pythonhosted.org/packages/b8/94/daff920e82c1b70e3618a2ac39fbc01ae3e2ff6124e80739ce5d71c9b920/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:032efa2674356903cd0261c4317a561a6850f3ac864a63fc1583147fb05a79b0", size = 289395, upload-time = "2025-10-06T05:37:02.115Z" }, + { url = "https://files.pythonhosted.org/packages/e3/20/bba307ab4235a09fdcd3cc5508dbabd17c4634a1af4b96e0f69bfe551ebd/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6da155091429aeba16851ecb10a9104a108bcd32f6c1642867eadaee401c1c41", size = 283659, upload-time = "2025-10-06T05:37:03.711Z" }, + { url = "https://files.pythonhosted.org/packages/fd/00/04ca1c3a7a124b6de4f8a9a17cc2fcad138b4608e7a3fc5877804b8715d7/frozenlist-1.8.0-cp313-cp313t-win32.whl", hash = "sha256:0f96534f8bfebc1a394209427d0f8a63d343c9779cda6fc25e8e121b5fd8555b", size = 43492, upload-time = "2025-10-06T05:37:04.915Z" }, + { url = "https://files.pythonhosted.org/packages/59/5e/c69f733a86a94ab10f68e496dc6b7e8bc078ebb415281d5698313e3af3a1/frozenlist-1.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5d63a068f978fc69421fb0e6eb91a9603187527c86b7cd3f534a5b77a592b888", size = 48034, upload-time = "2025-10-06T05:37:06.343Z" }, + { url = "https://files.pythonhosted.org/packages/16/6c/be9d79775d8abe79b05fa6d23da99ad6e7763a1d080fbae7290b286093fd/frozenlist-1.8.0-cp313-cp313t-win_arm64.whl", hash = "sha256:bf0a7e10b077bf5fb9380ad3ae8ce20ef919a6ad93b4552896419ac7e1d8e042", size = 41749, upload-time = "2025-10-06T05:37:07.431Z" }, + { url = "https://files.pythonhosted.org/packages/f1/c8/85da824b7e7b9b6e7f7705b2ecaf9591ba6f79c1177f324c2735e41d36a2/frozenlist-1.8.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cee686f1f4cadeb2136007ddedd0aaf928ab95216e7691c63e50a8ec066336d0", size = 86127, upload-time = "2025-10-06T05:37:08.438Z" }, + { url = "https://files.pythonhosted.org/packages/8e/e8/a1185e236ec66c20afd72399522f142c3724c785789255202d27ae992818/frozenlist-1.8.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:119fb2a1bd47307e899c2fac7f28e85b9a543864df47aa7ec9d3c1b4545f096f", size = 49698, upload-time = "2025-10-06T05:37:09.48Z" }, + { url = "https://files.pythonhosted.org/packages/a1/93/72b1736d68f03fda5fdf0f2180fb6caaae3894f1b854d006ac61ecc727ee/frozenlist-1.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4970ece02dbc8c3a92fcc5228e36a3e933a01a999f7094ff7c23fbd2beeaa67c", size = 49749, upload-time = "2025-10-06T05:37:10.569Z" }, + { url = "https://files.pythonhosted.org/packages/a7/b2/fabede9fafd976b991e9f1b9c8c873ed86f202889b864756f240ce6dd855/frozenlist-1.8.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:cba69cb73723c3f329622e34bdbf5ce1f80c21c290ff04256cff1cd3c2036ed2", size = 231298, upload-time = "2025-10-06T05:37:11.993Z" }, + { url = "https://files.pythonhosted.org/packages/3a/3b/d9b1e0b0eed36e70477ffb8360c49c85c8ca8ef9700a4e6711f39a6e8b45/frozenlist-1.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:778a11b15673f6f1df23d9586f83c4846c471a8af693a22e066508b77d201ec8", size = 232015, upload-time = "2025-10-06T05:37:13.194Z" }, + { url = "https://files.pythonhosted.org/packages/dc/94/be719d2766c1138148564a3960fc2c06eb688da592bdc25adcf856101be7/frozenlist-1.8.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0325024fe97f94c41c08872db482cf8ac4800d80e79222c6b0b7b162d5b13686", size = 225038, upload-time = "2025-10-06T05:37:14.577Z" }, + { url = "https://files.pythonhosted.org/packages/e4/09/6712b6c5465f083f52f50cf74167b92d4ea2f50e46a9eea0523d658454ae/frozenlist-1.8.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:97260ff46b207a82a7567b581ab4190bd4dfa09f4db8a8b49d1a958f6aa4940e", size = 240130, upload-time = "2025-10-06T05:37:15.781Z" }, + { url = "https://files.pythonhosted.org/packages/f8/d4/cd065cdcf21550b54f3ce6a22e143ac9e4836ca42a0de1022da8498eac89/frozenlist-1.8.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:54b2077180eb7f83dd52c40b2750d0a9f175e06a42e3213ce047219de902717a", size = 242845, upload-time = "2025-10-06T05:37:17.037Z" }, + { url = "https://files.pythonhosted.org/packages/62/c3/f57a5c8c70cd1ead3d5d5f776f89d33110b1addae0ab010ad774d9a44fb9/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2f05983daecab868a31e1da44462873306d3cbfd76d1f0b5b69c473d21dbb128", size = 229131, upload-time = "2025-10-06T05:37:18.221Z" }, + { url = "https://files.pythonhosted.org/packages/6c/52/232476fe9cb64f0742f3fde2b7d26c1dac18b6d62071c74d4ded55e0ef94/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:33f48f51a446114bc5d251fb2954ab0164d5be02ad3382abcbfe07e2531d650f", size = 240542, upload-time = "2025-10-06T05:37:19.771Z" }, + { url = "https://files.pythonhosted.org/packages/5f/85/07bf3f5d0fb5414aee5f47d33c6f5c77bfe49aac680bfece33d4fdf6a246/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:154e55ec0655291b5dd1b8731c637ecdb50975a2ae70c606d100750a540082f7", size = 237308, upload-time = "2025-10-06T05:37:20.969Z" }, + { url = "https://files.pythonhosted.org/packages/11/99/ae3a33d5befd41ac0ca2cc7fd3aa707c9c324de2e89db0e0f45db9a64c26/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:4314debad13beb564b708b4a496020e5306c7333fa9a3ab90374169a20ffab30", size = 238210, upload-time = "2025-10-06T05:37:22.252Z" }, + { url = "https://files.pythonhosted.org/packages/b2/60/b1d2da22f4970e7a155f0adde9b1435712ece01b3cd45ba63702aea33938/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:073f8bf8becba60aa931eb3bc420b217bb7d5b8f4750e6f8b3be7f3da85d38b7", size = 231972, upload-time = "2025-10-06T05:37:23.5Z" }, + { url = "https://files.pythonhosted.org/packages/3f/ab/945b2f32de889993b9c9133216c068b7fcf257d8595a0ac420ac8677cab0/frozenlist-1.8.0-cp314-cp314-win32.whl", hash = "sha256:bac9c42ba2ac65ddc115d930c78d24ab8d4f465fd3fc473cdedfccadb9429806", size = 40536, upload-time = "2025-10-06T05:37:25.581Z" }, + { url = "https://files.pythonhosted.org/packages/59/ad/9caa9b9c836d9ad6f067157a531ac48b7d36499f5036d4141ce78c230b1b/frozenlist-1.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:3e0761f4d1a44f1d1a47996511752cf3dcec5bbdd9cc2b4fe595caf97754b7a0", size = 44330, upload-time = "2025-10-06T05:37:26.928Z" }, + { url = "https://files.pythonhosted.org/packages/82/13/e6950121764f2676f43534c555249f57030150260aee9dcf7d64efda11dd/frozenlist-1.8.0-cp314-cp314-win_arm64.whl", hash = "sha256:d1eaff1d00c7751b7c6662e9c5ba6eb2c17a2306ba5e2a37f24ddf3cc953402b", size = 40627, upload-time = "2025-10-06T05:37:28.075Z" }, + { url = "https://files.pythonhosted.org/packages/c0/c7/43200656ecc4e02d3f8bc248df68256cd9572b3f0017f0a0c4e93440ae23/frozenlist-1.8.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:d3bb933317c52d7ea5004a1c442eef86f426886fba134ef8cf4226ea6ee1821d", size = 89238, upload-time = "2025-10-06T05:37:29.373Z" }, + { url = "https://files.pythonhosted.org/packages/d1/29/55c5f0689b9c0fb765055629f472c0de484dcaf0acee2f7707266ae3583c/frozenlist-1.8.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:8009897cdef112072f93a0efdce29cd819e717fd2f649ee3016efd3cd885a7ed", size = 50738, upload-time = "2025-10-06T05:37:30.792Z" }, + { url = "https://files.pythonhosted.org/packages/ba/7d/b7282a445956506fa11da8c2db7d276adcbf2b17d8bb8407a47685263f90/frozenlist-1.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2c5dcbbc55383e5883246d11fd179782a9d07a986c40f49abe89ddf865913930", size = 51739, upload-time = "2025-10-06T05:37:32.127Z" }, + { url = "https://files.pythonhosted.org/packages/62/1c/3d8622e60d0b767a5510d1d3cf21065b9db874696a51ea6d7a43180a259c/frozenlist-1.8.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:39ecbc32f1390387d2aa4f5a995e465e9e2f79ba3adcac92d68e3e0afae6657c", size = 284186, upload-time = "2025-10-06T05:37:33.21Z" }, + { url = "https://files.pythonhosted.org/packages/2d/14/aa36d5f85a89679a85a1d44cd7a6657e0b1c75f61e7cad987b203d2daca8/frozenlist-1.8.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92db2bf818d5cc8d9c1f1fc56b897662e24ea5adb36ad1f1d82875bd64e03c24", size = 292196, upload-time = "2025-10-06T05:37:36.107Z" }, + { url = "https://files.pythonhosted.org/packages/05/23/6bde59eb55abd407d34f77d39a5126fb7b4f109a3f611d3929f14b700c66/frozenlist-1.8.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2dc43a022e555de94c3b68a4ef0b11c4f747d12c024a520c7101709a2144fb37", size = 273830, upload-time = "2025-10-06T05:37:37.663Z" }, + { url = "https://files.pythonhosted.org/packages/d2/3f/22cff331bfad7a8afa616289000ba793347fcd7bc275f3b28ecea2a27909/frozenlist-1.8.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cb89a7f2de3602cfed448095bab3f178399646ab7c61454315089787df07733a", size = 294289, upload-time = "2025-10-06T05:37:39.261Z" }, + { url = "https://files.pythonhosted.org/packages/a4/89/5b057c799de4838b6c69aa82b79705f2027615e01be996d2486a69ca99c4/frozenlist-1.8.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:33139dc858c580ea50e7e60a1b0ea003efa1fd42e6ec7fdbad78fff65fad2fd2", size = 300318, upload-time = "2025-10-06T05:37:43.213Z" }, + { url = "https://files.pythonhosted.org/packages/30/de/2c22ab3eb2a8af6d69dc799e48455813bab3690c760de58e1bf43b36da3e/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:168c0969a329b416119507ba30b9ea13688fafffac1b7822802537569a1cb0ef", size = 282814, upload-time = "2025-10-06T05:37:45.337Z" }, + { url = "https://files.pythonhosted.org/packages/59/f7/970141a6a8dbd7f556d94977858cfb36fa9b66e0892c6dd780d2219d8cd8/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:28bd570e8e189d7f7b001966435f9dac6718324b5be2990ac496cf1ea9ddb7fe", size = 291762, upload-time = "2025-10-06T05:37:46.657Z" }, + { url = "https://files.pythonhosted.org/packages/c1/15/ca1adae83a719f82df9116d66f5bb28bb95557b3951903d39135620ef157/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b2a095d45c5d46e5e79ba1e5b9cb787f541a8dee0433836cea4b96a2c439dcd8", size = 289470, upload-time = "2025-10-06T05:37:47.946Z" }, + { url = "https://files.pythonhosted.org/packages/ac/83/dca6dc53bf657d371fbc88ddeb21b79891e747189c5de990b9dfff2ccba1/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:eab8145831a0d56ec9c4139b6c3e594c7a83c2c8be25d5bcf2d86136a532287a", size = 289042, upload-time = "2025-10-06T05:37:49.499Z" }, + { url = "https://files.pythonhosted.org/packages/96/52/abddd34ca99be142f354398700536c5bd315880ed0a213812bc491cff5e4/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:974b28cf63cc99dfb2188d8d222bc6843656188164848c4f679e63dae4b0708e", size = 283148, upload-time = "2025-10-06T05:37:50.745Z" }, + { url = "https://files.pythonhosted.org/packages/af/d3/76bd4ed4317e7119c2b7f57c3f6934aba26d277acc6309f873341640e21f/frozenlist-1.8.0-cp314-cp314t-win32.whl", hash = "sha256:342c97bf697ac5480c0a7ec73cd700ecfa5a8a40ac923bd035484616efecc2df", size = 44676, upload-time = "2025-10-06T05:37:52.222Z" }, + { url = "https://files.pythonhosted.org/packages/89/76/c615883b7b521ead2944bb3480398cbb07e12b7b4e4d073d3752eb721558/frozenlist-1.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:06be8f67f39c8b1dc671f5d83aaefd3358ae5cdcf8314552c57e7ed3e6475bdd", size = 49451, upload-time = "2025-10-06T05:37:53.425Z" }, + { url = "https://files.pythonhosted.org/packages/e0/a3/5982da14e113d07b325230f95060e2169f5311b1017ea8af2a29b374c289/frozenlist-1.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:102e6314ca4da683dca92e3b1355490fed5f313b768500084fbe6371fddfdb79", size = 42507, upload-time = "2025-10-06T05:37:54.513Z" }, + { url = "https://files.pythonhosted.org/packages/9a/9a/e35b4a917281c0b8419d4207f4334c8e8c5dbf4f3f5f9ada73958d937dcc/frozenlist-1.8.0-py3-none-any.whl", hash = "sha256:0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d", size = 13409, upload-time = "2025-10-06T05:38:16.721Z" }, +] + +[[package]] +name = "gravis" +version = "0.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "setuptools" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/73/76/10b6264a45fffb565b9ad26b9fbc4ba91c2ca700f8dbfba84dcdebf5cbdd/gravis-0.1.0.tar.gz", hash = "sha256:a36342602aa7da3bbd674c970439717926a8f7ca0678aa82cc5d756e8c2a529b", size = 648634, upload-time = "2021-12-08T21:43:19.456Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9b/fb/6a1659424cfab2d8df49f3f02ca11bf7ca303775681f105239b556c9146c/gravis-0.1.0-py3-none-any.whl", hash = "sha256:ccfb4dbba13c7570f1f33b35ab5d536868acf5ce8a0c427fd898340cf4e414f4", size = 659146, upload-time = "2021-12-08T21:43:15.348Z" }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httptools" +version = "0.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/e5/d471fcb0e14523fe1c3f4ba58ca52480e7bd70ad7109a3846bc75892f7fb/httptools-0.8.0.tar.gz", hash = "sha256:6b2a32f18d97e16e90827d7a819ffa8dbd8cc245fc4e1fa9d1095b54ef4bd999", size = 271342, upload-time = "2026-05-25T22:17:48.841Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f8/d2/c3eedaef57de65c3cc5f8dc244cf12d09c84ad258a479055aad6db23206c/httptools-0.8.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ed377e64805bdba4943c82717333f8f8603a13b09aff9cead2717c6c817fb168", size = 208428, upload-time = "2026-05-25T22:16:59.717Z" }, + { url = "https://files.pythonhosted.org/packages/f1/94/dfe435d90d0ef61ec0f2cc3d480eef78c59727c6c2ce039f433882f6131a/httptools-0.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9518c406d7b310f05adb1a37f80acabac40504a575d7c0da6d3e365c695ac20d", size = 113366, upload-time = "2026-05-25T22:17:00.795Z" }, + { url = "https://files.pythonhosted.org/packages/cc/d4/13025f1a56e615dcb331e0bbe2d9a1143212b58c263385fc5d2e558f5bac/httptools-0.8.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:57278e6fa0424c42a8a3e454828ab4f0aff27b40cddf9679579b98c6dce6a376", size = 464676, upload-time = "2026-05-25T22:17:02.014Z" }, + { url = "https://files.pythonhosted.org/packages/bf/95/4c1c26c0b985f8a3331682d802598f14e32dc41bf7509266eb2c04ad4801/httptools-0.8.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bbb8caadb2b742d293169d2b458b5c001ef70e3158704aa3d3ef9597624c5d1d", size = 464235, upload-time = "2026-05-25T22:17:03.109Z" }, + { url = "https://files.pythonhosted.org/packages/a2/82/6735be2b0ca527718c431cdb8e5f70c3862c0844a687df0f572c51e11497/httptools-0.8.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:52dd695b865fe96d9d2b16b64a895f3f57bf3cb064e8383cd3b5713a069e8085", size = 449809, upload-time = "2026-05-25T22:17:04.443Z" }, + { url = "https://files.pythonhosted.org/packages/b5/f9/5811c74f37a758c8a4aa3dc430375119d335947e883efc4664d8f3559a41/httptools-0.8.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:20b4aac66ff65f7db06a375808b78f42a94970aa22e826b3cb2b43eb09174124", size = 452174, upload-time = "2026-05-25T22:17:05.476Z" }, + { url = "https://files.pythonhosted.org/packages/cc/94/97b75870dea07b71e3ec535cebe525b08d723152e4c7d13fa887e51f4de2/httptools-0.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:a1b4c8e7a489a0d750d91894e9a8cdc295838f1924c0ca903ae993456fddec07", size = 90991, upload-time = "2026-05-25T22:17:06.75Z" }, + { url = "https://files.pythonhosted.org/packages/14/88/1d21a36da8f5cb0fa49eafd4b169eba5608d57e75bbcf61845cbc6243216/httptools-0.8.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:880490234c10f70a9830743097e8958d6e4b9f5a0ffc24515023afeef984054d", size = 208247, upload-time = "2026-05-25T22:17:07.843Z" }, + { url = "https://files.pythonhosted.org/packages/a5/42/cc4feea2945cb3051038f090c9b36bd5b8a9d7f5a894a506a8983e33fd1c/httptools-0.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5931891fb7b441b8a3853cf1b85c82c903defce084dd5f6771ca46e31bf862c5", size = 113064, upload-time = "2026-05-25T22:17:09.136Z" }, + { url = "https://files.pythonhosted.org/packages/e3/a6/febbb8b8db0f58b38e44ad6cb946e6a255ae49b55f2e8543408fb7501ccd/httptools-0.8.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b15fc622b0f869d19207c4089a501d9bcc63ca5e071ffdd2f03f922df882dcb2", size = 523851, upload-time = "2026-05-25T22:17:10.106Z" }, + { url = "https://files.pythonhosted.org/packages/b7/e4/f90a0df0b83beff265b7e3b65f2a4cefd95792d4be0ac3e16049f2acd3c2/httptools-0.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:425f83884fd6343828d8c565f046cb72b6d19063f6924093e11bcd8e1548cd09", size = 518842, upload-time = "2026-05-25T22:17:11.218Z" }, + { url = "https://files.pythonhosted.org/packages/9e/2d/0c9ac76dd2c893841fbf6498d6acec4f2442e1b7067f6e3e316a80e494e8/httptools-0.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ef7c3c97f4311c7be57e2986629df89d49cb434dbff78eafcd48c2bff986b15a", size = 501238, upload-time = "2026-05-25T22:17:12.728Z" }, + { url = "https://files.pythonhosted.org/packages/ca/42/906adc91ae3a5fa9c59c0a2f21c139725bd7e5b41ae6acd485cd14123ebf/httptools-0.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a1afd7c9fbff0d9f5d489c4ce2768bd09c84a46ddefc7161e6aa82ae35c85745", size = 509567, upload-time = "2026-05-25T22:17:13.842Z" }, + { url = "https://files.pythonhosted.org/packages/05/0b/4240efeb672751ee5b9b380cb0e3fdc050bc05f68adc7a8aefc4fcd9a69a/httptools-0.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:cd96f29b4bab1d42fa6e3d008711c75e0f79e94e06827330160e3a304227f150", size = 90918, upload-time = "2026-05-25T22:17:15.155Z" }, + { url = "https://files.pythonhosted.org/packages/5e/e5/8cfcabc5546e8022f168be28bcdaa128a240a0befdd03b59d558b4f18bd6/httptools-0.8.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:614ceea8ea606848bece2338ac03b3ce5324bcb4be8dc7d377ed708012fa4db8", size = 205148, upload-time = "2026-05-25T22:17:16.333Z" }, + { url = "https://files.pythonhosted.org/packages/2a/0e/0fb14848c19a686c8062ff9067c1a48793e3224b47bc5b201535b6036fce/httptools-0.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2d689918c15a013c65ef52d9fd495d766893ab831a2c8d89f2ac5940a5df847c", size = 111368, upload-time = "2026-05-25T22:17:17.586Z" }, + { url = "https://files.pythonhosted.org/packages/2e/1b/46f1cecf06b9bbde8e4b8c88034ac7908989e5ff7a3a388ef38392949c1f/httptools-0.8.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:eb3028cca2fc0a6d720e52ef61d8ebb62fcbfeb1de56874546d858d3f25a26b7", size = 486447, upload-time = "2026-05-25T22:17:18.564Z" }, + { url = "https://files.pythonhosted.org/packages/77/00/258bfc0837221f81d9725c45f9b948a6a6b2994a147a4fb66e85100c668f/httptools-0.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:88bdd940f2b5d487b4d032c6afa5489a7dc4694410d43de3c38c4fb3af0dc45d", size = 482448, upload-time = "2026-05-25T22:17:19.912Z" }, + { url = "https://files.pythonhosted.org/packages/04/ab/d1cef3b5523f4d272a70f42a776c3169a2dddfe3a54de4b2ce4a36341528/httptools-0.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6a43c9dd399758ccc0531acb0a3c4a6c299ee893ee9400e9c893b7bdcfae0681", size = 464460, upload-time = "2026-05-25T22:17:20.882Z" }, + { url = "https://files.pythonhosted.org/packages/ce/48/5d1d072442277bb2b3434e0e60690b8e8c23840ef7de8b6ea54040a536d3/httptools-0.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0770728beb05094c809b98e814edff5fef69d26ad7d21185f2f6d5884a0ba683", size = 471312, upload-time = "2026-05-25T22:17:22.085Z" }, + { url = "https://files.pythonhosted.org/packages/0d/66/b96623b27e51a68199ef4efdda0613cced9233fe3062ac74e50749c5ad37/httptools-0.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:7685df791fad561384bfb139e77fde27a1ffd93134e016f95a0db424ffbf77b1", size = 90117, upload-time = "2026-05-25T22:17:23.074Z" }, + { url = "https://files.pythonhosted.org/packages/1a/12/fa3fbf5f9517b273edea2dc982aa82a8c634091e67c590792b729017bc6f/httptools-0.8.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:de242a49b5d18e0a8776e654e9f6bf6d89f3875a5c35b425a0e7ce940feb3fd6", size = 206183, upload-time = "2026-05-25T22:17:24.004Z" }, + { url = "https://files.pythonhosted.org/packages/30/fc/5e7c4cb443370f2090a3aba0453a07384d29ff66b7435bb90e77e1037599/httptools-0.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:159e9ab5f701ccd42e555a12f1ad8ff69702910fc1c996cf2bb66e5fcb7a231b", size = 112079, upload-time = "2026-05-25T22:17:25.216Z" }, + { url = "https://files.pythonhosted.org/packages/ba/53/771bd891eb0f236f32145d6a1775777ec85745f3cc983a1f23d1a3b8ddfe/httptools-0.8.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c4a9f1707e4823d54dfec6c33fa3697d302aed536ed352a7ebb5a061ddb869d0", size = 481596, upload-time = "2026-05-25T22:17:26.186Z" }, + { url = "https://files.pythonhosted.org/packages/62/42/94e15bc68ce3d423243c45d7f1b0c7561f13844f97dc52ae23182fb65628/httptools-0.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d76ad7b951387e3632c8716a9bb03ac5b45c5f16119aa409db0459520887944e", size = 480865, upload-time = "2026-05-25T22:17:27.542Z" }, + { url = "https://files.pythonhosted.org/packages/1c/7c/fe2980fc03723272e30f135b62360b075f513dfe7cc73aef36c7f04012bd/httptools-0.8.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a3b7387147361c3fd47a0bde763c5c91b5b4cd4dc9989b8ece84ff436c99843b", size = 463189, upload-time = "2026-05-25T22:17:28.546Z" }, + { url = "https://files.pythonhosted.org/packages/15/1b/47fc5fff68acd1bfa20b4734059c9a06cadb88119dcd5258b5b0d21d91c8/httptools-0.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f256d6ce930c52ca1cb2a960b7da03548c454e7d28b06059ad41bfe789036ce0", size = 466610, upload-time = "2026-05-25T22:17:29.816Z" }, + { url = "https://files.pythonhosted.org/packages/60/bd/07b13c93ffd9bec9546e0d43f8e19378dd696dbd278511406bc07371ef1f/httptools-0.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:19d1ee275bb59ba2643ba9a3a1e51cc0c788caf2b8df506368e03f56fdd08527", size = 92705, upload-time = "2026-05-25T22:17:31.133Z" }, + { url = "https://files.pythonhosted.org/packages/fd/c4/121648f68ce066d7bd762d6b6d97e620847642d38d54f3d90ff11d947629/httptools-0.8.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:de1ed58a974e75d56560acc7e7fed01a454994429456f65209789992e41f2568", size = 215023, upload-time = "2026-05-25T22:17:32.401Z" }, + { url = "https://files.pythonhosted.org/packages/b9/b0/312a062ae741ae3e8baa8c8bf20be81b2e67337b259ab4349bebc7b6142e/httptools-0.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e93c227b595c6926c1acee96891dd9da4be338cfbe82e5cd3bb9d8dd7dc4ac0b", size = 117405, upload-time = "2026-05-25T22:17:33.742Z" }, + { url = "https://files.pythonhosted.org/packages/fc/37/fccd705f795386bb05bf413012fecff2a33e5aa8c2f069096de3e9fd8702/httptools-0.8.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2a021c3a8e65cc125390d72f59b968afca3bdcaff25bd67965e0a055a14946ca", size = 558497, upload-time = "2026-05-25T22:17:34.732Z" }, + { url = "https://files.pythonhosted.org/packages/bd/39/f172e8003576de35f5ba77ff417cf0e34429d35dc014deef15afa337a72c/httptools-0.8.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:48774d39cbb70e2b1f71f88852a3087ae1d3a1eb80482bb48c13067ab080c14f", size = 571585, upload-time = "2026-05-25T22:17:35.813Z" }, + { url = "https://files.pythonhosted.org/packages/3e/b9/f5564760af99f3dbbf3f9104dc00e5da27e96cf433c6bdcf77617f70bf3f/httptools-0.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:88eead8ec8680a9f146c655bc88445a325bd7921cfd8194c7337e9467282427d", size = 543297, upload-time = "2026-05-25T22:17:37.08Z" }, + { url = "https://files.pythonhosted.org/packages/99/67/8d9f2c313618e161b82f3873188e7196126da1d6e29688df40eb3997c77a/httptools-0.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:2c032fa028f46871ec7e1fc59fc15e8023eab3e6bbe6ece786a1611719a5d081", size = 539535, upload-time = "2026-05-25T22:17:38.032Z" }, + { url = "https://files.pythonhosted.org/packages/48/63/b906c01e53f50d432c0defe43ce52764a111dc1bdd028bafbeb54dcfd008/httptools-0.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:384c17174464c8e873398b7af24f0b1f44d992c820328413951a625323155d77", size = 108209, upload-time = "2026-05-25T22:17:39.473Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + +[[package]] +name = "idna" +version = "3.19" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5f/f7/abb373e5757eaec4b922b92f97ec8d6d7e057cf06778247604fbc4e7c3f3/idna-3.19.tar.gz", hash = "sha256:5e0811a4383b21dc5838069f801c4fb62113b7447663d2530d2bd6e77b49bf15", size = 215237, upload-time = "2026-08-18T05:14:24.27Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/57/b0/0e52c878c53f245edd3a11020f20979b3f490f245af532c7cae3027754b5/idna-3.19-py3-none-any.whl", hash = "sha256:815e7be7a7806d54abb586dc943addc79e8b2ee16915059658cbeff4b1b43bf4", size = 68550, upload-time = "2026-08-18T05:14:22.343Z" }, +] + +[[package]] +name = "importlib-metadata" +version = "9.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "zipp" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6f/7e/1e7e8dc30634b93ebb3d58a3dea569ad146e656218d3960ab04f62047b29/importlib_metadata-9.0.1.tar.gz", hash = "sha256:ab830580bc0ef3db61ce8fae716389e5462b67e033018bab6d8f80ef17172f99", size = 59124, upload-time = "2026-08-28T15:30:34.646Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/55/ecca97ae19075f1fac62def77731e7f535e6c1fb8f92ff08160c5e6dade8/importlib_metadata-9.0.1-py3-none-any.whl", hash = "sha256:bba5600596a7e21f3eef53281cf28d6a5195634d2f2b78ff9501a3272c6eaab0", size = 27920, upload-time = "2026-08-28T15:30:33.433Z" }, +] + +[[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 = "inquirerpy" +version = "0.3.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pfzy" }, + { name = "prompt-toolkit" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/64/73/7570847b9da026e07053da3bbe2ac7ea6cde6bb2cbd3c7a5a950fa0ae40b/InquirerPy-0.3.4.tar.gz", hash = "sha256:89d2ada0111f337483cb41ae31073108b2ec1e618a49d7110b0d7ade89fc197e", size = 44431, upload-time = "2022-06-27T23:11:20.598Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/ff/3b59672c47c6284e8005b42e84ceba13864aa0f39f067c973d1af02f5d91/InquirerPy-0.3.4-py3-none-any.whl", hash = "sha256:c65fdfbac1fa00e3ee4fb10679f4d3ed7a012abf4833910e63c295827fe2a7d4", size = 67677, upload-time = "2022-06-27T23:11:17.723Z" }, +] + +[[package]] +name = "jaraco-classes" +version = "3.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "more-itertools" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/c0/ed4a27bc5571b99e3cff68f8a9fa5b56ff7df1c2251cc715a652ddd26402/jaraco.classes-3.4.0.tar.gz", hash = "sha256:47a024b51d0239c0dd8c8540c6c7f484be3b8fcf0b2d85c13825780d3b3f3acd", size = 11780, upload-time = "2024-03-31T07:27:36.643Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/66/b15ce62552d84bbfcec9a4873ab79d993a1dd4edb922cbfccae192bd5b5f/jaraco.classes-3.4.0-py3-none-any.whl", hash = "sha256:f662826b6bed8cace05e7ff873ce0f9283b5c924470fe664fff1c2f00f581790", size = 6777, upload-time = "2024-03-31T07:27:34.792Z" }, +] + +[[package]] +name = "jaraco-context" +version = "6.1.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "backports-tarfile", marker = "python_full_version < '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/af/50/4763cd07e722bb6285316d390a164bc7e479db9d90daa769f22578f698b4/jaraco_context-6.1.2.tar.gz", hash = "sha256:f1a6c9d391e661cc5b8d39861ff077a7dc24dc23833ccee564b234b81c82dfe3", size = 16801, upload-time = "2026-03-20T22:13:33.922Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f2/58/bc8954bda5fcda97bd7c19be11b85f91973d67a706ed4a3aec33e7de22db/jaraco_context-6.1.2-py3-none-any.whl", hash = "sha256:bf8150b79a2d5d91ae48629d8b427a8f7ba0e1097dd6202a9059f29a36379535", size = 7871, upload-time = "2026-03-20T22:13:32.808Z" }, +] + +[[package]] +name = "jaraco-functools" +version = "4.6.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "more-itertools" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6c/1f/c23395957d41ccf27c4e535c3d334c4051e5395b3752057ba4cbaec35c56/jaraco_functools-4.6.0.tar.gz", hash = "sha256:880c577ec9720b3a052d5bc611fb9f2269b3d87902ef42440df443b88e443280", size = 20837, upload-time = "2026-07-14T01:28:02.544Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/36/ecc85bc96c273dc8a11273ed4782272975e6338d4a3e9228621175edf0e3/jaraco_functools-4.6.0-py3-none-any.whl", hash = "sha256:99e3dc0060c5cbe8fcd1cdb36258e2a65ca40f1566b2033b12abb1bb44dd3c30", size = 11677, upload-time = "2026-07-14T01:28:01.59Z" }, +] + +[[package]] +name = "jeepney" +version = "0.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7b/6f/357efd7602486741aa73ffc0617fb310a29b588ed0fd69c2399acbb85b0c/jeepney-0.9.0.tar.gz", hash = "sha256:cf0e9e845622b81e4a28df94c40345400256ec608d0e55bb8a3feaa9163f5732", size = 106758, upload-time = "2025-02-27T18:51:01.684Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b2/a3/e137168c9c44d18eff0376253da9f1e9234d0239e0ee230d2fee6cea8e55/jeepney-0.9.0-py3-none-any.whl", hash = "sha256:97e5714520c16fc0a45695e5365a2e11b81ea79bba796e26f9f1d178cb182683", size = 49010, upload-time = "2025-02-27T18:51:00.104Z" }, +] + +[[package]] +name = "jmespath" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d3/59/322338183ecda247fb5d1763a6cbe46eff7222eaeebafd9fa65d4bf5cb11/jmespath-1.1.0.tar.gz", hash = "sha256:472c87d80f36026ae83c6ddd0f1d05d4e510134ed462851fd5f754c8c3cbb88d", size = 27377, upload-time = "2026-01-22T16:35:26.279Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/2f/967ba146e6d58cf6a652da73885f52fc68001525b4197effc174321d70b4/jmespath-1.1.0-py3-none-any.whl", hash = "sha256:a5663118de4908c91729bea0acadca56526eb2698e83de10cd116ae0f4e97c64", size = 20419, upload-time = "2026-01-22T16:35:24.919Z" }, +] + +[[package]] +name = "keyring" +version = "25.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "importlib-metadata", marker = "python_full_version < '3.12'" }, + { name = "jaraco-classes" }, + { name = "jaraco-context" }, + { name = "jaraco-functools" }, + { name = "jeepney", marker = "sys_platform == 'linux'" }, + { name = "pywin32-ctypes", marker = "sys_platform == 'win32'" }, + { name = "secretstorage", marker = "sys_platform == 'linux'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/43/4b/674af6ef2f97d56f0ab5153bf0bfa28ccb6c3ed4d1babf4305449668807b/keyring-25.7.0.tar.gz", hash = "sha256:fe01bd85eb3f8fb3dd0405defdeac9a5b4f6f0439edbb3149577f244a2e8245b", size = 63516, upload-time = "2025-11-16T16:26:09.482Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/db/e655086b7f3a705df045bf0933bdd9c2f79bb3c97bfef1384598bb79a217/keyring-25.7.0-py3-none-any.whl", hash = "sha256:be4a0b195f149690c166e850609a477c532ddbfbaed96a404d4e43f8d5e2689f", size = 39160, upload-time = "2025-11-16T16:26:08.402Z" }, +] + +[[package]] +name = "lqp" +version = "0.5.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/23/18/aa9c6ee6e0d7a2f18dbe7ed95611648e01437057ef5ab1f8f37d38937874/lqp-0.5.7.tar.gz", hash = "sha256:874b292ee38ea99707d3170ca952c62454a731fc12bded779475c07f0039f6ba", size = 94209, upload-time = "2026-07-28T03:23:12.789Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bd/8f/9366e77ea43014ff19cf681317e1212de5ae428c1f9c39d4877b1f5de151/lqp-0.5.7-py3-none-any.whl", hash = "sha256:cf9ab6cdf65b34a13df773e654527a537063b056f725f601b042ebdeba11ead2", size = 88087, upload-time = "2026-07-28T03:23:11.378Z" }, +] + +[[package]] +name = "markdown-it-py" +version = "4.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/ff/7841249c247aa650a76b9ee4bbaeae59370dc8bfd2f6c01f3630c35eb134/markdown_it_py-4.2.0.tar.gz", hash = "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49", size = 82454, upload-time = "2026-05-07T12:08:28.36Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a", size = 91687, upload-time = "2026-05-07T12:08:27.182Z" }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + +[[package]] +name = "more-itertools" +version = "11.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/de/1d/f4da6f02cdffe04d6362210b807146a26044c88d839208aec273bb0d9184/more_itertools-11.1.0.tar.gz", hash = "sha256:48e8f4d9e7e5878571ecf6f2b4e57634f93cd474cc8cfbd2376f2d11b396e30d", size = 145772, upload-time = "2026-05-22T14:14:29.909Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/3d/1087453384dbde46a8c7f9356eead2c58be8a7bf156bca40243377c85715/more_itertools-11.1.0-py3-none-any.whl", hash = "sha256:4b65538ae22f6fed0ce4874efd317463a7489796a0939fa66824dd542125a192", size = 72226, upload-time = "2026-05-22T14:14:28.824Z" }, +] + +[[package]] +name = "multidict" +version = "6.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/14/95/989c1b5ca17b72128661530cd6e351a0a83cda9a4d6c036e9ed976c18931/multidict-6.8.0.tar.gz", hash = "sha256:5cd4637ce76312ba1e05eb9c5193fec231f64fee0944e135fa1e951242355b37", size = 122412, upload-time = "2026-09-09T13:57:57.967Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d4/37/90216392620b6ef8704eb0bc055141745de396121067e98f1f72bdac33c3/multidict-6.8.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:1b8986d4313dcee7c932837d16a535f1840b827bac1ea7c5c4c80751d0423794", size = 85033, upload-time = "2026-09-09T13:53:18.786Z" }, + { url = "https://files.pythonhosted.org/packages/2b/bb/e01b8cf906479b2fa046e992b84a9d9f39c1ed4058acc353004cd9a04df9/multidict-6.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2622fe114c0bd66ca5c461859357587f5a5e35ee5ff49fc5643d1bc78dbb41c6", size = 51008, upload-time = "2026-09-09T13:53:20.044Z" }, + { url = "https://files.pythonhosted.org/packages/c5/da/35d70c920812d9ddc6f295f6426457665194335fbc35aa0b96716aba219f/multidict-6.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:26a7aafc992e78872e2c8c1f7248c0e01139cf9020a7781b0c064fa566832712", size = 50232, upload-time = "2026-09-09T13:53:21.356Z" }, + { url = "https://files.pythonhosted.org/packages/91/6b/4c988a7c0daa4fbffc6080ed3c37b3a67cf225ba1de69d10a19ca1dd8d0d/multidict-6.8.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:05c2e90c5289c5f7436ba2c25812a5fbdaa1c1bc11c8d8d3bbf64f5cd7c633dd", size = 271678, upload-time = "2026-09-09T13:53:22.716Z" }, + { url = "https://files.pythonhosted.org/packages/73/2b/22c7de8a72fc5c36390e8049d86d842b032ac7c87ade035a3dafb7df4ffc/multidict-6.8.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7941ef106ca1f2c62314a13c7ed913bcf49641f3efdc12864d588e17870920ac", size = 270283, upload-time = "2026-09-09T13:53:24.235Z" }, + { url = "https://files.pythonhosted.org/packages/ce/f4/c1428318f945c57c016ba690338af41f87f18a7d3a7ef3227b1440a2c169/multidict-6.8.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a5a7ee1217949ddd43c6b7bcf70d5c22193bb50e8c695386de5905325e93ce9f", size = 245306, upload-time = "2026-09-09T13:53:25.656Z" }, + { url = "https://files.pythonhosted.org/packages/8b/92/a37f7519fb32b0bf43b0540292effe60edaf0691959214b227795bd3d56a/multidict-6.8.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6b62b7e0025aa48dec11e125e655d1157985a5fdcec04b1ad500101ad072b891", size = 279567, upload-time = "2026-09-09T13:53:27.162Z" }, + { url = "https://files.pythonhosted.org/packages/51/fe/a93c2ce417401863cc88ecf6561577625c140990d412e37f347a1c03a144/multidict-6.8.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ea880d441be7c510106bc56064be39266d948aef94ad4955e8784690019a5d9f", size = 282526, upload-time = "2026-09-09T13:53:28.927Z" }, + { url = "https://files.pythonhosted.org/packages/f2/9d/6bb4f84fdd82acfa09dc312ac133e7f76cbf2370004447f2a90e65e5d63f/multidict-6.8.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ab83fdd8cf307353edba9c427c17a3a021c2522d690f5633dd9f72d28b48ccca", size = 272604, upload-time = "2026-09-09T13:53:30.595Z" }, + { url = "https://files.pythonhosted.org/packages/3a/97/df0a30a4d786d313f24b39cb96edaaa3bbfe83a0b309577c81a797783ec5/multidict-6.8.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3d1f48582686a0a3b81e9b43234766cc96697df72081af3f48107bd3f34d34e5", size = 250786, upload-time = "2026-09-09T13:53:32.15Z" }, + { url = "https://files.pythonhosted.org/packages/6f/72/e59a917680d00214ba41f9fec19a8bec48f3bdf62656bc4377f37ae30947/multidict-6.8.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:563661919f603374c40cf45ffcd25535c12b8954203569a2ab1cee5265871cf4", size = 265419, upload-time = "2026-09-09T13:53:33.943Z" }, + { url = "https://files.pythonhosted.org/packages/47/21/0eb8868982ff07c1a2faaef7502ee0be32ef247dc1bf27881c51e7f4b20d/multidict-6.8.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:2ba9933e8f35fe4a70f540b837254c4055da82dc3a9e500a8f95e61498083a15", size = 258934, upload-time = "2026-09-09T13:53:35.662Z" }, + { url = "https://files.pythonhosted.org/packages/fa/a6/0586396716faf950c10ffbe733e4a57b4eeda9f7073e60c09bd4a05a766e/multidict-6.8.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:68d40b2bace413f3231f5729d3fcfb1837fd31c4907e241b5d43211bfd76f3c2", size = 273168, upload-time = "2026-09-09T13:53:37.131Z" }, + { url = "https://files.pythonhosted.org/packages/31/79/7197af20190d0d832be3b18582be46c224f64f5ba1cd35d32068d6af31ed/multidict-6.8.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:a9e246f67ac038568b854ed7c5578e4c6af1f742359901a8fcc3603ff1358df6", size = 275884, upload-time = "2026-09-09T13:53:38.579Z" }, + { url = "https://files.pythonhosted.org/packages/f7/f7/af60573e25ffecc09e805464580d579338cf1a44332ab52757038435ed60/multidict-6.8.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:29b6e7bc4442a56cf8e0dc1cabf3fdc77cd533568d6829fc76a1effd2ce332ec", size = 246967, upload-time = "2026-09-09T13:53:40.172Z" }, + { url = "https://files.pythonhosted.org/packages/6b/02/4459f8c5025ab034d3d9af9a34bbde11319016cff2f327362c8ec43a80a1/multidict-6.8.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:82780eb8bf59e8fb25dd081fde6e058805045d6374a7f2f877effc826ca4434b", size = 272358, upload-time = "2026-09-09T13:53:41.868Z" }, + { url = "https://files.pythonhosted.org/packages/51/99/680d3522ab51a77094d31d7958c9f5989499a01ffbe5aebe11d25212b385/multidict-6.8.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:2196ba6df392c3574acadd14ef87550f3611349c8618564de324b806a7a31cee", size = 267450, upload-time = "2026-09-09T13:53:43.646Z" }, + { url = "https://files.pythonhosted.org/packages/4a/04/d0c773805b0aea171287b01a82e4c28c59ef2c2d93e8047394765181363f/multidict-6.8.0-cp311-cp311-win32.whl", hash = "sha256:b8b7aa75146266fd3e2a2437cf69ae188688c04ab8665b163d4257b46c1e0c83", size = 46847, upload-time = "2026-09-09T13:53:45.161Z" }, + { url = "https://files.pythonhosted.org/packages/71/f8/1a959771a4dcd3224bd7bb40054f66b98ba5b20d6b74fd273f548f887e0a/multidict-6.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:b03ca066b47b18b205cc080dca6f76cbd159f8cdd33a02a0700164c13b37e463", size = 51549, upload-time = "2026-09-09T13:53:46.448Z" }, + { url = "https://files.pythonhosted.org/packages/64/7c/3a74b11599a9d8f3cfbb78b9c5cac3ff3cdc17278e4c00329c7c18dcaff9/multidict-6.8.0-cp311-cp311-win_arm64.whl", hash = "sha256:54af1266710cb0f305127ae0b970aff8d208057f8a29cd6e1db99b0114947035", size = 48020, upload-time = "2026-09-09T13:53:47.767Z" }, + { url = "https://files.pythonhosted.org/packages/13/83/a4621577679149ea001806f5963f3fc687c391c1bd5217157be2278863f5/multidict-6.8.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:22a310ad37672a261e55a8b5e28d0ae08cfb68abb1f46418ccd19835c3b8e836", size = 84146, upload-time = "2026-09-09T13:53:49.163Z" }, + { url = "https://files.pythonhosted.org/packages/09/00/236b063f3e606055a3a9ba8faa5d40e6c688b059a58056b055f213476f46/multidict-6.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bb8c7da8c861391f7ae48e3593762be2dabe405109e01aec520fbe1a6d15d14b", size = 51049, upload-time = "2026-09-09T13:53:50.46Z" }, + { url = "https://files.pythonhosted.org/packages/91/9d/954b139bfa969855f2d4cb5ae7b7d44dd7106f754305b6e21a9068213aa7/multidict-6.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:10456943903744ae1249728161c96bd9d2f7eb5ee17fcc2ffda2dc32e1bb36c7", size = 49362, upload-time = "2026-09-09T13:53:51.878Z" }, + { url = "https://files.pythonhosted.org/packages/d7/8a/8774f5b3f6d5266ecd1117876e04b405f0f1ce19aa750b35a826efe6cfe4/multidict-6.8.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:658f5a1895b804423d97b22d06fc0d0b171c7c01dcc3aa9c8faf0c0e26a249a5", size = 278619, upload-time = "2026-09-09T13:53:53.44Z" }, + { url = "https://files.pythonhosted.org/packages/db/47/736080fec911ed9f2dd57ccab5a8145e4f17c4987de0bfc27bee20e4d170/multidict-6.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:90c10b22860dbd09982d0b8993b66231a861bea2993d4a817ff35273f6ea285a", size = 283771, upload-time = "2026-09-09T13:53:55.048Z" }, + { url = "https://files.pythonhosted.org/packages/4c/d5/b7f41f59b0583f092602308a5e7c16ec5efd00d60214b22511e89a38dd19/multidict-6.8.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:088b04a66b3c1fce6fe4d771ec184a0426262d0b86709c908477b4ac7965df40", size = 262108, upload-time = "2026-09-09T13:53:56.631Z" }, + { url = "https://files.pythonhosted.org/packages/54/b2/a52dc06c6e2598672308e3d392fd85b837b23c25dda459bedaea84985080/multidict-6.8.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9caef53b20a105c0d66518a34be2f71b2783de8d091767575ef86f6ea422236d", size = 289899, upload-time = "2026-09-09T13:53:58.415Z" }, + { url = "https://files.pythonhosted.org/packages/a9/21/00cda7983f37d119b86f1f89d5b4cf771ecb6d0fedeb9a0971758d6d6d4a/multidict-6.8.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a5e1583c14775580da05641240ce0d93f36ce3ddef3d5083a827468b0bcfe874", size = 293025, upload-time = "2026-09-09T13:53:59.973Z" }, + { url = "https://files.pythonhosted.org/packages/c6/c7/4544cc02e45bbfac4d8788b05379bb360021fd8c53fa74b0f624126ac188/multidict-6.8.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:003a3bddb32915c3f67096ea41d24e53edf710edb65a1f5d0c70ab40b0e4d20b", size = 287410, upload-time = "2026-09-09T13:54:01.652Z" }, + { url = "https://files.pythonhosted.org/packages/43/1a/7abed90b8eba381842235bfa6f4d730204fd7deb374fc87e3ec9b2c2b4ac/multidict-6.8.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:35977263d9bf506dbc65349f63b3b8c91606d4abc110990945e3b94bc671319c", size = 255878, upload-time = "2026-09-09T13:54:03.366Z" }, + { url = "https://files.pythonhosted.org/packages/25/3e/73fae10e15fc4d711975337caff7e494c87de5d0189afe3518b21b945326/multidict-6.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e9dc7b4ff6ef184504b49ef9a4113d49a646653b2ce89f5f48c1f57cdf6ba081", size = 277831, upload-time = "2026-09-09T13:54:04.963Z" }, + { url = "https://files.pythonhosted.org/packages/c5/cf/01cfc81492933331147004861bdff201d8adeba8485ecd8f490e755fe7e8/multidict-6.8.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:887f9a975996032c686719eb7b3e1e7942fab5079c2b778bbd9afe9a9d78244f", size = 275096, upload-time = "2026-09-09T13:54:06.661Z" }, + { url = "https://files.pythonhosted.org/packages/de/59/e9a3773b17297fa1e38fd4b3c6f5f2f458380796be62eca7d0d77c250618/multidict-6.8.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:f3071e6515cc63714d014da8f738ae9fa3997c476203f3cd46de380c2376ed7b", size = 279803, upload-time = "2026-09-09T13:54:08.389Z" }, + { url = "https://files.pythonhosted.org/packages/64/9d/2d712a2605b3971908e3b4f5eb6f98c353d9991e106f684d0e08ae581814/multidict-6.8.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c5f3a2af441670d80ce5fdf13b6c1b421fc1fc7fc5182d58ac7486738bb2b742", size = 284595, upload-time = "2026-09-09T13:54:10.17Z" }, + { url = "https://files.pythonhosted.org/packages/58/6c/21aded8586e552b29892268c576e5745d1a894c5451c9866ca3c06b7ec50/multidict-6.8.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:31e8901637e20ccb3cf8f8848b5d0f7a00462bf5b34f7cf3dcbb2753b18e8b39", size = 252641, upload-time = "2026-09-09T13:54:11.811Z" }, + { url = "https://files.pythonhosted.org/packages/2a/70/56a415ae0a45e5eae2ec817d46aeb72a1ae777863621c85f1f39d329275b/multidict-6.8.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:13967dca8b2f33230a1427b52438326bb1c9101a1df22a3309ed3fcbbb3c96f0", size = 283369, upload-time = "2026-09-09T13:54:13.59Z" }, + { url = "https://files.pythonhosted.org/packages/08/7e/7b7cd611fd94bf2f6bd16244c50495867ba394d5baaf8e6e487d39494ab3/multidict-6.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad474c11d851b6fc97cb625e4822bc0cbd567fc07dc2602e28faec5a36b42bbb", size = 281653, upload-time = "2026-09-09T13:54:15.174Z" }, + { url = "https://files.pythonhosted.org/packages/33/4a/b19a5892ef2ef6c68ae278b4f1504b82e01037baedd92c55d37e55ecad00/multidict-6.8.0-cp312-cp312-win32.whl", hash = "sha256:7bb0dad75068fee80fcb60f88569722c199d8656a16706702dc6e3b786819c90", size = 47936, upload-time = "2026-09-09T13:54:16.638Z" }, + { url = "https://files.pythonhosted.org/packages/29/00/1952f9f282aa71e7c3db3a6b47afb689d0ddf283dbded7e6326a91d421c9/multidict-6.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:7d26dc8f070c0ec5579e987fa615ffd6883086106eefdff9e10d160fc5630630", size = 51723, upload-time = "2026-09-09T13:54:18.05Z" }, + { url = "https://files.pythonhosted.org/packages/49/b5/c9d57dbafe25b8f3460ce2961c968539a81ff7a70160c44dcfd4255cbcd1/multidict-6.8.0-cp312-cp312-win_arm64.whl", hash = "sha256:e6ec7d37841609a691b96a10b4fde386c7cd93ebbb939f59c9f23325ee788395", size = 48492, upload-time = "2026-09-09T13:54:19.42Z" }, + { url = "https://files.pythonhosted.org/packages/84/1f/d7112c2dd7db02677097be72fb65542f51a5aa73cb472b87ec211ba9e0dd/multidict-6.8.0-cp313-cp313-android_24_x86_64.whl", hash = "sha256:ec0a4d066356054d569a66e0a94691a2058b680be5e710298f61db11a3c4609f", size = 54197, upload-time = "2026-09-09T13:54:20.814Z" }, + { url = "https://files.pythonhosted.org/packages/ae/24/876015abbcb4a179d946579eb77b778eb5a948fc8381bc7928ba895bc051/multidict-6.8.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:714597cb5d5e15a8a449d2ae23c45b486a9e8fa33c462c7a33d7f35b65d92943", size = 47787, upload-time = "2026-09-09T13:54:22.51Z" }, + { url = "https://files.pythonhosted.org/packages/06/c1/ceb7d25f8a567599db2eb19b08cac58d67ff553cff42dcadbea9aba56a20/multidict-6.8.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:e0db3a4d1e264e225037a6023888972c25206a96e016021a5bea41c9a939f2a9", size = 48815, upload-time = "2026-09-09T13:54:23.986Z" }, + { url = "https://files.pythonhosted.org/packages/18/e3/e1c6e9c3818c34b782f23ce5fdba3eaa34ec6750dc53078dfac80fa59be7/multidict-6.8.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:27747162712e85c84598d364425dbf1714ff335bdb6ba3171c4e5081196e8916", size = 83484, upload-time = "2026-09-09T13:54:25.674Z" }, + { url = "https://files.pythonhosted.org/packages/4a/a0/c23f78a4badee9a5b3e760495c661c62a92c340a1dfd00f829cd16e256bb/multidict-6.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:847d6082ae694dc95e548acb201bc100e1cfa96513bc71fdcb86f709dad6c435", size = 50763, upload-time = "2026-09-09T13:54:27.135Z" }, + { url = "https://files.pythonhosted.org/packages/c4/fe/db552d402a3f6b650f5d3ae11b82b93833836aebb51bcda22d8691121129/multidict-6.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:88a6df88567680504ae28bfa7a1f2f64243d91e79a40b2c92ef42efc531e23da", size = 49029, upload-time = "2026-09-09T13:54:28.483Z" }, + { url = "https://files.pythonhosted.org/packages/01/b4/546853fba19dcef77cdf91fc173faf0b02284a49106cf250511166b4ec5c/multidict-6.8.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:560b211fc3bd4a1e1c6de44f6d38113bf5b410dfc89a4c0d2a3c0edbf1a0dfb8", size = 278863, upload-time = "2026-09-09T13:54:30.145Z" }, + { url = "https://files.pythonhosted.org/packages/ee/3f/4b52dac7db547936eb762123ac1d99df23f92fdb358bae600e322f611247/multidict-6.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:202436df907c15adbb94360296c425ea53cf8968a5d2cff9b5b9790ae1972b33", size = 283915, upload-time = "2026-09-09T13:54:31.937Z" }, + { url = "https://files.pythonhosted.org/packages/fd/6e/c0dfbf170e49a91bcb9ce850d51cb98357f3033c5227529200ca7625853e/multidict-6.8.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c46a08bf070d6849fed483e9d9833f9d06aecb8382ed985be0b38508b3ae958e", size = 260704, upload-time = "2026-09-09T13:54:33.529Z" }, + { url = "https://files.pythonhosted.org/packages/91/02/56973a060ab8dfc2e80bb6797682f6577aff7123cdb1de1a568670ae3499/multidict-6.8.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2cd560498ae8e1bcc955643c1d78eb8e338226d07a983c656ea8c4443d3eec0f", size = 290243, upload-time = "2026-09-09T13:54:35.408Z" }, + { url = "https://files.pythonhosted.org/packages/d5/67/69112989f131bdea4a87b74e82cb0a2daf37880cd92b0e6f0420020adceb/multidict-6.8.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:758233648ac47b07c575224c4eadd73c8929c3b4c31e2afcfea935fde1cda735", size = 291131, upload-time = "2026-09-09T13:54:37.205Z" }, + { url = "https://files.pythonhosted.org/packages/c2/75/9435f68b0cfc442d4917de85c26f2b2e1292630883414a25576083fa2469/multidict-6.8.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:122adc7c46ac1e31ecfc7f81b2530533dccafdba70f5d741649f87e336c63384", size = 287551, upload-time = "2026-09-09T13:54:38.835Z" }, + { url = "https://files.pythonhosted.org/packages/13/08/2ee4838081d6587849611aa7ec722c4cb2469e912fd0eaee980e7bac064c/multidict-6.8.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8125e60f3c70e323ac07dd8b3635f7b3bbc5c3a9ac04ae5988f668ff7ae28a18", size = 254591, upload-time = "2026-09-09T13:54:40.806Z" }, + { url = "https://files.pythonhosted.org/packages/94/f1/05673b51191f77f4198b8e4b35f16ea71c0300c72ca8aa027a66a61b6edc/multidict-6.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:83ff054b04915be5c15680da6c6012474a2cc2bf534129a0e8c6a99f17ba7238", size = 278204, upload-time = "2026-09-09T13:54:42.672Z" }, + { url = "https://files.pythonhosted.org/packages/45/4f/b6cf74322b3fbd3e011a1e903730191922291a7779f6d404114c2189b806/multidict-6.8.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:930c6058047410e3edff445f5a6e4457f2e089042dede00e2d18ce06f3ceae2e", size = 275600, upload-time = "2026-09-09T13:54:44.348Z" }, + { url = "https://files.pythonhosted.org/packages/1b/ab/958bbb04377159ff03c7314cd9d8a48dd6fc4f78c840589c22ab155ee9c7/multidict-6.8.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:13e26f59f0eecfc5f67c663ad550ffdaf62c0f657547cde387f6c86af1c9449e", size = 279793, upload-time = "2026-09-09T13:54:46.086Z" }, + { url = "https://files.pythonhosted.org/packages/a0/3a/706605ab0dfc4179748ee7949829e63c6f14ae28667aceeefaf2c701807f/multidict-6.8.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:fd789a294d8e098528be29b2669b83005ce569339f8cef167fc0274c3115c34c", size = 284751, upload-time = "2026-09-09T13:54:47.793Z" }, + { url = "https://files.pythonhosted.org/packages/b3/a5/567e36c013ad023546de633079c6b22101dd43226b193cba00e6399703be/multidict-6.8.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:3126f2a96704505aa4e92a72d6e8a5d7f29d40a987ced8bf69e29d71dfc71fbc", size = 250812, upload-time = "2026-09-09T13:54:49.509Z" }, + { url = "https://files.pythonhosted.org/packages/0d/5f/6b0b64aa0cd346b07831dabaa6ccda0e73014c5df044b68baa763f0f0552/multidict-6.8.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:23c9ee89967b6a9b4048acb3b93b660ed714ce9c8bf3bbe652959bc120dc02dc", size = 281606, upload-time = "2026-09-09T13:54:51.288Z" }, + { url = "https://files.pythonhosted.org/packages/31/8c/b846b6796f26d496efb07fedef2b69f6de533da32a56f12d236722a96157/multidict-6.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7a62e302fc8cd6aa8972207e7e951d1fdee7c1dda18568305041d19f0e2c00f5", size = 281733, upload-time = "2026-09-09T13:54:53.05Z" }, + { url = "https://files.pythonhosted.org/packages/f0/f3/bf14a39d4af5697fd9404baaf70a0aeeb82d258b95de5cb16b1a7f98ae6f/multidict-6.8.0-cp313-cp313-win32.whl", hash = "sha256:093167d22a8c95af30f597b8a5686f20a14512989942d4be804d119899caca20", size = 47738, upload-time = "2026-09-09T13:54:54.676Z" }, + { url = "https://files.pythonhosted.org/packages/19/0a/598511a5741a3cb374971b3b02eda8a09896118ba528a54795f7e7e8bfb4/multidict-6.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:f25b61a708bd276e8cbb6afcbbf1b8e793a3be70ba0a842d0b8692020f83b706", size = 51609, upload-time = "2026-09-09T13:54:56.38Z" }, + { url = "https://files.pythonhosted.org/packages/fd/b7/6f5c1bd4ffe42d4a6db0f2f65491d4088e9c25c990358fb31a614621d664/multidict-6.8.0-cp313-cp313-win_arm64.whl", hash = "sha256:bb36381e1f9f9d06eba2f10bdd438e5d20c07d5b55e1a3eee30b9f44cbf52316", size = 48280, upload-time = "2026-09-09T13:54:58.03Z" }, + { url = "https://files.pythonhosted.org/packages/ab/85/153341590e233a967c1d6791a83402d01693dec0f4c1f695606ef16c7ed2/multidict-6.8.0-cp314-cp314-android_24_x86_64.whl", hash = "sha256:f8b09b25e0f4dc2ea9e2adbb1cc3ba11a94d6fa3dd978ae659c8743052e1afbc", size = 53758, upload-time = "2026-09-09T13:54:59.563Z" }, + { url = "https://files.pythonhosted.org/packages/fd/ff/44f72d516ece0398683ef52061797d83a74b16b8c1e4587408e97959d783/multidict-6.8.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:1f57c414be82490bc0e0305fdb834186229b2d9b6a35fa0afd1eb1a772d125ab", size = 47495, upload-time = "2026-09-09T13:55:01.382Z" }, + { url = "https://files.pythonhosted.org/packages/50/5f/6e118f761b024dd35d26c2fe7ba41572bb0e8ac5f8cfccbbcbc2ff76da4e/multidict-6.8.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:00be37bde741bf60871082cd347a093218c44886e99231b7516671c70f2c280d", size = 48540, upload-time = "2026-09-09T13:55:02.989Z" }, + { url = "https://files.pythonhosted.org/packages/e8/4b/3eed744491b32f0e318e7db89dc06858732362f706e8d045fa9ab51a343a/multidict-6.8.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:e37b744849fb631bb52e3dadde35ffeee365a6c41cf71257b5b7acc9cd83fd38", size = 83130, upload-time = "2026-09-09T13:55:04.554Z" }, + { url = "https://files.pythonhosted.org/packages/6d/de/95c2c0ddcccb9a41ffbaa5df8ea059a8ff81916b7617a8847ecd89ed8061/multidict-6.8.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:c2b2a96cf1dd99fe7867be4c013314225f4d5786e6685906e29932d42aca6f11", size = 50574, upload-time = "2026-09-09T13:55:06.387Z" }, + { url = "https://files.pythonhosted.org/packages/f5/b7/f4f4989594f99bc121ad9277090c4e49819b08ab1a96e132b628a9e10b7d/multidict-6.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:bea7df027015856ba5d0a88e3b4777ff8cb5c66b58fc108050fe79d4dd9d4d2d", size = 48786, upload-time = "2026-09-09T13:55:08.131Z" }, + { url = "https://files.pythonhosted.org/packages/b2/86/f1d86a0222f31fb3df8eef3d6c9abf7e8d65d49edd8d0d7e7afaf23d23cc/multidict-6.8.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d3da668e903c934ed0b587ecacfed6901f6ae6384a6e975887592b61845e78bc", size = 276670, upload-time = "2026-09-09T13:55:09.803Z" }, + { url = "https://files.pythonhosted.org/packages/03/50/6945c50f86a978b2bcace9ca344165ff80883be47d984489bbba8fa0ab20/multidict-6.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:64eaeda36ee8d88f9e8616a587a8c66a663283cf6e0dcf013c1ddd8c758e4aef", size = 279339, upload-time = "2026-09-09T13:55:11.685Z" }, + { url = "https://files.pythonhosted.org/packages/ee/2c/e649889ba23fd1f4442a85427b99d9e6261226b2ac31914aa7f5b241d947/multidict-6.8.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:ac746cb365bac1c462da9e3e6ab8904a8efe2217a56b0b2e3d9480f41d2b2602", size = 252549, upload-time = "2026-09-09T13:55:13.527Z" }, + { url = "https://files.pythonhosted.org/packages/e5/f8/1023b66e011b1395fb160dabb0f0608ef67e569f0bdb2c1d5ac9b2f2adc6/multidict-6.8.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:18f0e06360c3e451a3ab800355773c8d125a758238d780c800b0ee5e90ee903c", size = 286203, upload-time = "2026-09-09T13:55:15.19Z" }, + { url = "https://files.pythonhosted.org/packages/7e/6c/48aea545cbda6d0444848ec23d988c13b86538a00a1b7d3868cc2382ff94/multidict-6.8.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:69708fecaa88bcb2341397b49fc95057a835b02a3670c551b37f95dd79e64e3a", size = 285039, upload-time = "2026-09-09T13:55:16.928Z" }, + { url = "https://files.pythonhosted.org/packages/68/2a/066123b17291671bf67d2a5c65ee81a48de53913bd1b1578791519eacdb0/multidict-6.8.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9606f583e7acaf61e7b3f56074e14037b9af7cb194590edfc0114b3ae5931ff7", size = 281075, upload-time = "2026-09-09T13:55:19.155Z" }, + { url = "https://files.pythonhosted.org/packages/47/20/4f0b2c485da2e8a659cc677717a3745872918c9c85064491a1ef75d7a3bf/multidict-6.8.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1f66fe6a021173d0d47968491791966b9f3e6d61115f2491744aa0c07a6e67af", size = 250431, upload-time = "2026-09-09T13:55:21.07Z" }, + { url = "https://files.pythonhosted.org/packages/ff/c7/b9a288901577aa0b82c33c64d52246c88076d260ad7b6c16b021ca0f8e99/multidict-6.8.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:be007d1aee2cbd530347dcafedb400891a3b5f1bd7135f95cf5d5b330b5219ee", size = 273891, upload-time = "2026-09-09T13:55:22.887Z" }, + { url = "https://files.pythonhosted.org/packages/da/51/0ba50cab2cfd067988de2abb73f23076ac727fe18d03f1368a59def64727/multidict-6.8.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:8457aff3c12a89a8e1c4674de5c777857fbc429f40fe117a3d29538547cbc364", size = 265262, upload-time = "2026-09-09T13:55:24.77Z" }, + { url = "https://files.pythonhosted.org/packages/0f/d6/e5be1117dbca6eb9ce231142b7e20599418bb3500147db51bf844ce8afcb/multidict-6.8.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:95c27b4f3f04320fc44e338573f40c5c956b504a7fcf081a157fd0b02579311c", size = 278033, upload-time = "2026-09-09T13:55:26.67Z" }, + { url = "https://files.pythonhosted.org/packages/d2/28/cad0afaec3caa56ea2c1ceed43c164d62ad3e83e950daf0d0c87bcf9dca7/multidict-6.8.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:d244cf6b52b5ba1c34c3832f4652a668ebb36d95949b96eed9a1c54d916a90dd", size = 281717, upload-time = "2026-09-09T13:55:28.569Z" }, + { url = "https://files.pythonhosted.org/packages/c9/d2/025702df0b69b856db70a4d66f77622f51c3d99771ec9a07f3ca80f7e098/multidict-6.8.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:5bbbb696c8024475b1877d14ce20d5f1cc05b8f6d786cea0fe3aa7fedc02e891", size = 247124, upload-time = "2026-09-09T13:55:30.497Z" }, + { url = "https://files.pythonhosted.org/packages/b4/96/9dddca563f06a921956389c0bc9b894355b98b0bdf62299e2560c50afb6d/multidict-6.8.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:cbd86f9787c5e2f5fd27d8b21458222f107347c6731c4e93dde68f554b466a2d", size = 275954, upload-time = "2026-09-09T13:55:32.57Z" }, + { url = "https://files.pythonhosted.org/packages/ec/91/8b2f1f2a774a955665f268340a2b59db7020c5f12baac02ae9ef1b1660cf/multidict-6.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2f8a4b0b4d639d525928c7f30de527bfdf9ead6e44a5e8cb9c50aced5e4590cb", size = 275508, upload-time = "2026-09-09T13:55:34.368Z" }, + { url = "https://files.pythonhosted.org/packages/b6/1a/e2cabdfc0880a61a99d2b8bc361035036fb5a2c6af31ea3fa054ba1065c5/multidict-6.8.0-cp314-cp314-win32.whl", hash = "sha256:8890c89d662560e51c55ac1304d6f919b23942abe9ae1127cb1de9aa6132fa52", size = 46938, upload-time = "2026-09-09T13:55:36.057Z" }, + { url = "https://files.pythonhosted.org/packages/b9/7c/11234bcba62c22a58f2ba168499cfe3531f49de3edd5090d04a8c6cdc936/multidict-6.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:45cc39ba50fb0754a4359b90f8229ae08598fe2266abe3521b4e5a9ba916534a", size = 50291, upload-time = "2026-09-09T13:55:37.698Z" }, + { url = "https://files.pythonhosted.org/packages/ab/61/793668439df924752a8137d6db0de97ed1add494779b01e4764dfc60571b/multidict-6.8.0-cp314-cp314-win_arm64.whl", hash = "sha256:d0264f8d5cb0a803f650a6a8572dfa0cd1e099a2234c588dc8fb220b415b865f", size = 47622, upload-time = "2026-09-09T13:55:39.335Z" }, + { url = "https://files.pythonhosted.org/packages/9c/b8/3c091b929e6b5b2f6e0eba2232178e76d4503c8b96b92dfc281ff1d823be/multidict-6.8.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:1969971900b0871530f9b62280dcc2d75688e74d2a69262bc01faf2b96c78f04", size = 88789, upload-time = "2026-09-09T13:55:41.086Z" }, + { url = "https://files.pythonhosted.org/packages/9e/d7/3df83fab22dd64615db71e3b3cc1346b581d1459719637ce52144f9f6558/multidict-6.8.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:8180b635290a75af8478f1b3e9810135381ae24833293fe77b85c1c21ff842ab", size = 53399, upload-time = "2026-09-09T13:55:42.685Z" }, + { url = "https://files.pythonhosted.org/packages/2d/78/41bd04c04b0aed16540c4856c9e012afc1c254298da154398308df05e26a/multidict-6.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:4261863fc8b5ab1b815ede94e592e94c6af5b04616014929057e61859e7382a9", size = 51597, upload-time = "2026-09-09T13:55:44.569Z" }, + { url = "https://files.pythonhosted.org/packages/2f/6b/7bc4cdddf624e1e7e0231734b1331729ea46df10d7c8fd3fce79756e7d0e/multidict-6.8.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0b143d53590e89f43153d81d505a8448d4d57354354385aef8a51d67ffefa27e", size = 264391, upload-time = "2026-09-09T13:55:46.548Z" }, + { url = "https://files.pythonhosted.org/packages/dc/b6/d2a946e5938771e92c39354563e535ef6bc6dfe399dd4307c6df8dfea183/multidict-6.8.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:da1c112c5784ccd9d32cd90be6739fee32644e874eff6ae8f0497cba3e352e58", size = 264680, upload-time = "2026-09-09T13:55:49.915Z" }, + { url = "https://files.pythonhosted.org/packages/33/6b/3f9e981c42e7eb9329918523f0f9362ceb0ac3ee0ee1165c28f674249d75/multidict-6.8.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:f7eefd0233a7c33ca980a5cfef26f1e9b5e2137839e752a99963696729f12d91", size = 235420, upload-time = "2026-09-09T13:55:51.92Z" }, + { url = "https://files.pythonhosted.org/packages/bc/e1/a3a33a039fb6d381800ae5d1d587b697b8c27fcdfe48819420f08703acba/multidict-6.8.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:348bb85e2038b40c007383616d73f734869063772372519549ebd7da1723d1a4", size = 270309, upload-time = "2026-09-09T13:55:54.023Z" }, + { url = "https://files.pythonhosted.org/packages/95/5d/8b06724a957f2e480f159b9550988a67810fbe9555a09c5f6a2a4b829607/multidict-6.8.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:095f62ea4e7a3be2f6c567ab695ce10e950f2adb905c1bec82281593e0b2d2ad", size = 275169, upload-time = "2026-09-09T13:55:55.948Z" }, + { url = "https://files.pythonhosted.org/packages/ab/32/8f3dfe2ffa5d0df2a95f71e63c2f11fe3b5e1771f26ef73bb1af84de83f8/multidict-6.8.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:be569fff1d85cd29391c431c5641c8772acb75bbdc61e60a8e82fceb9023d385", size = 264900, upload-time = "2026-09-09T13:55:57.803Z" }, + { url = "https://files.pythonhosted.org/packages/6b/73/d5829fc00a055d6ab445e0876346ee9cdee670766cd4190dc0a496188c0f/multidict-6.8.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3533a03e4e789baf6a286e7b0b1b6da3f3d7c3eab569686ee29ee1d8b52e2cb4", size = 242486, upload-time = "2026-09-09T13:56:00.002Z" }, + { url = "https://files.pythonhosted.org/packages/b7/58/e8d7874038e31e0533182d1c3c5331a856b9c849a71bb26a21850e8c91e1/multidict-6.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1bdb9b8fba5a9aef673ec90db3f55b1ce743f2fbdea4d37dc04d14ccdfc153ff", size = 259916, upload-time = "2026-09-09T13:56:01.802Z" }, + { url = "https://files.pythonhosted.org/packages/4d/f8/1b56a7401acda20efc016440f4fad3bef66c4aee54ca080ec143881ebb0d/multidict-6.8.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:f8d7b66c9e09c0bb0add2b5895e646b62a0849e71155066f215523de6b95cbe6", size = 251209, upload-time = "2026-09-09T13:56:03.767Z" }, + { url = "https://files.pythonhosted.org/packages/bd/5b/68d67a9e302b0645a747ba910c30eb41f2834fcdc1d85f53eae2dfceee0a/multidict-6.8.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:563d6500ca80dac7bba6f48a78e0ffd87e21a7d4d24642c6503a2ddccd70c110", size = 264505, upload-time = "2026-09-09T13:56:05.795Z" }, + { url = "https://files.pythonhosted.org/packages/18/13/4dc304ba2c5f5307b474ab2ce1ed1f6b02b0b4e233c182e3981ed436c2e3/multidict-6.8.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:346ac52e56bcda320c0dcdfdd081947ed7cada33afea4e2284bef7b0733bff9b", size = 264916, upload-time = "2026-09-09T13:56:09.079Z" }, + { url = "https://files.pythonhosted.org/packages/dc/0f/7b1f729d18369915009185201be5d0b8df0e525340fe6a600d2f8441d6cf/multidict-6.8.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:4ee953a5ebaeed38dc21cc032ed17a9d9782802e00042200497ab4b01b0bf7c0", size = 236839, upload-time = "2026-09-09T13:56:11.273Z" }, + { url = "https://files.pythonhosted.org/packages/22/d1/eba1b88b18b7019d9136303fe77909257c40fabde5aaf138a4d900b6ce3c/multidict-6.8.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:2f79cc3e8039a8cf5c77e0811b0807953fd52d0863b9b76970b20d696dc64a78", size = 265307, upload-time = "2026-09-09T13:56:13.379Z" }, + { url = "https://files.pythonhosted.org/packages/aa/a6/6c1e4106faa27118ac612f4d664eaf909de252634785286262a627108e58/multidict-6.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:43a4b56555bbcf8af161e7c7682bd93eec10f068c95844511864c018c8e5e13b", size = 259041, upload-time = "2026-09-09T13:56:15.666Z" }, + { url = "https://files.pythonhosted.org/packages/c0/bc/ecfb8b6faa8e158a71b03bdf7f947f30e0bc5d899cc357573a76ab7bb1e5/multidict-6.8.0-cp314-cp314t-win32.whl", hash = "sha256:48ea524a25a1cd5972cf293bc95713918cba0bcd6fa9b992d906c857c546abe2", size = 50628, upload-time = "2026-09-09T13:56:17.837Z" }, + { url = "https://files.pythonhosted.org/packages/30/7f/e27fb699b70ad24dbd02ddee604658acb36f907c03c045baffe4ea774501/multidict-6.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d0be2b832435001bc623ca7f1499ca1a853d4f082fb61221a80ce71132f50b26", size = 55592, upload-time = "2026-09-09T13:56:19.652Z" }, + { url = "https://files.pythonhosted.org/packages/eb/7a/76de70b2f6733696803f1ee56abe44a3757a52777383032c7373d3fea0f4/multidict-6.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:62b8e291a4f7edbf7cde7a43d831d893ba443a1b627498b53581943b0e348feb", size = 50300, upload-time = "2026-09-09T13:56:21.516Z" }, + { url = "https://files.pythonhosted.org/packages/ce/32/4de7320ae032dc768090d11f708d2d386df3db04cb6b8b0db0230cfc66c3/multidict-6.8.0-cp315-cp315-android_24_x86_64.whl", hash = "sha256:e192018b732f7b168e6604cbdf40fa8e05c996693b9eb445a0d8a73f4b77c5d3", size = 53761, upload-time = "2026-09-09T13:56:23.192Z" }, + { url = "https://files.pythonhosted.org/packages/5c/45/ecb641309dc2cdc6040f18e22c68eb5e94398f9404c4365d810f4292e053/multidict-6.8.0-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:b25426f9f6ed402835617c8f23609a47045f91ecff365eb6734817e039a8ed25", size = 47505, upload-time = "2026-09-09T13:56:24.902Z" }, + { url = "https://files.pythonhosted.org/packages/eb/68/87d6161b9fef11943e0b894203da3fff561933ca3c9b2952b6e7100e9c9f/multidict-6.8.0-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:fa6c2880709c84457de104385b704fc28860f27e442ad13966fc4af8e714fe9c", size = 48549, upload-time = "2026-09-09T13:56:26.574Z" }, + { url = "https://files.pythonhosted.org/packages/97/f7/d852d2276407640cdbd29fe11cac6e93f70f59542cba174ef9d146738946/multidict-6.8.0-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:eabb03dc3e4ed6333ecd1cc9826ec80e7a98b5506deeb832d7260c8e44166d23", size = 83157, upload-time = "2026-09-09T13:56:28.227Z" }, + { url = "https://files.pythonhosted.org/packages/14/e3/16fe7ffa6090591d83cf6bc2486e77ce891705fb6d0191823140928311b5/multidict-6.8.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:59e539c4eb4d3a53b0e630a6ba2b2f2824732b5e73f90e30a280f12fde157b15", size = 50578, upload-time = "2026-09-09T13:56:30Z" }, + { url = "https://files.pythonhosted.org/packages/d0/0c/e38e41c1087a599f86ff58a01f358abf7c4db3c26a3e90eebb3e02193ef1/multidict-6.8.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:835d5a90b11d1f5f8200ff3cc8316bded76eebebc92436398947a27657e645e7", size = 48815, upload-time = "2026-09-09T13:56:32.056Z" }, + { url = "https://files.pythonhosted.org/packages/d3/f0/eb691f42af8e7775992f57904ec75dc356fc7cdc896e5f30879decdd26f2/multidict-6.8.0-cp315-cp315-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d2d236b8a44ae91536a12ebcb996bdb31cf27425f36b4d05c87f2ba2716050ba", size = 274804, upload-time = "2026-09-09T13:56:36.741Z" }, + { url = "https://files.pythonhosted.org/packages/87/05/28472ccfeb43c00a043c0385ca4294da21a5957859fb7860e2ebdb3e3011/multidict-6.8.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bb9a60b7faa5d37c426fa91cf4d6738182a1f2755b9fab7c9c64cd466c4ce51e", size = 279693, upload-time = "2026-09-09T13:56:38.531Z" }, + { url = "https://files.pythonhosted.org/packages/f3/a1/2b4fe73e5fecff807b47650a155c391a103136428cb21d6ba8e39c5912b5/multidict-6.8.0-cp315-cp315-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0ef606c15cac6c90279acf34120784b6f36662cbf382defd3955cd8f1115336b", size = 254969, upload-time = "2026-09-09T13:56:40.407Z" }, + { url = "https://files.pythonhosted.org/packages/44/e0/c97d1822783dfe52e02fd150fa3f02eb22410211a9e2615f71541803ed4b/multidict-6.8.0-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:68186a2d4051c8ffd17be33553bea2ec9bbc8ef860fe2980a221d96126296f31", size = 286392, upload-time = "2026-09-09T13:56:42.234Z" }, + { url = "https://files.pythonhosted.org/packages/2f/d9/772f1339e1d051236bcc137b0eac2b4aaaa0bbb56aaf924e9aaba901d9c1/multidict-6.8.0-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2cc66abb85e2108c9ff8a1c0d20fa260bf690bbb33caef4ff3ecb2c2cbdfff5d", size = 285348, upload-time = "2026-09-09T13:56:44.255Z" }, + { url = "https://files.pythonhosted.org/packages/06/ae/cd045747e4680362e02955a82c468e95b5e4d319e3a79574b3fb677de568/multidict-6.8.0-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:69b3e519a132bb943b0daae15fc8c2168706b17f826481d32a32a5e784b129e3", size = 282721, upload-time = "2026-09-09T13:56:46.088Z" }, + { url = "https://files.pythonhosted.org/packages/35/14/0802d9a3aae4ef21eaa39adbd729a380fa095932105e1424e417b53e783f/multidict-6.8.0-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e41226ecf607f062fe34a2f4cf64ad3a89e3a0180dc800b463b6b14c06dd10dc", size = 253168, upload-time = "2026-09-09T13:56:48.07Z" }, + { url = "https://files.pythonhosted.org/packages/b1/64/3f92298bab8fbe1332e708863fb55b66e755be6f416b3459720d48b33af9/multidict-6.8.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:610c7637bc36b90f39e6c66f710f93d57018f83d53e1e187caaa218c6892b95f", size = 274209, upload-time = "2026-09-09T13:56:50.023Z" }, + { url = "https://files.pythonhosted.org/packages/08/c2/2001ac0eac1a8b7390a5902d7115f66d4f256268057a502200b6ab12dad7/multidict-6.8.0-cp315-cp315-musllinux_1_2_armv7l.whl", hash = "sha256:65c85c79f5a2c04fbbc18f006c014674dc5fdf270cb978d8862c82c6f694e60c", size = 268044, upload-time = "2026-09-09T13:56:52.033Z" }, + { url = "https://files.pythonhosted.org/packages/0d/90/78a9e26c85f89abd562a67f7fcbaef9007fd5c37bb9efac19f1cf604e7c2/multidict-6.8.0-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:628ff11e6720f90acd0c305dfa3339f04a783a20de8cda6ac333ba46447261e8", size = 274806, upload-time = "2026-09-09T13:56:53.975Z" }, + { url = "https://files.pythonhosted.org/packages/3d/71/713bd445421b21531234c1f3630b768192cb9d80c8b1c5b05c5b505ff4c0/multidict-6.8.0-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:0935971bffd0b479fc90c4811ca787703e93fcb6afea939a375dfc80285ab368", size = 281890, upload-time = "2026-09-09T13:56:55.848Z" }, + { url = "https://files.pythonhosted.org/packages/de/a5/1387c538663e2dc8c27bbc7cd6955cb66de0f55c780cf7cd0fc06a1a16ca/multidict-6.8.0-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:9442b14eec262a1f74369bbd07e75bc5155105164649a4b9fbc1ebc7b8fb0b14", size = 249749, upload-time = "2026-09-09T13:56:58.01Z" }, + { url = "https://files.pythonhosted.org/packages/91/15/104296c9d70896b9759ce0812aa4899fab76d8b16bb32dcc5a78ab547c89/multidict-6.8.0-cp315-cp315-musllinux_1_2_s390x.whl", hash = "sha256:397599503b718f0137f26d3f6532d6955069cd2e5917c47ef581495bc2529ff8", size = 276138, upload-time = "2026-09-09T13:57:03.591Z" }, + { url = "https://files.pythonhosted.org/packages/f7/0a/f2a0c2658e9d7ff5964ec2820a02054558636fafd663230ddc8310b8ed39/multidict-6.8.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:9e37024b41d7a7e7e9cce14b248d54707c21c2a2ea30a47b71bdcefcafec00f2", size = 277077, upload-time = "2026-09-09T13:57:06.024Z" }, + { url = "https://files.pythonhosted.org/packages/98/50/bc46566caffba5c1c4a510519156371edf7c4ecd35c9ef917d0c1803487d/multidict-6.8.0-cp315-cp315-win32.whl", hash = "sha256:071da134651b04a8507dfb331ac0988f376337c2aea59486bf20989fb5b5a64e", size = 46930, upload-time = "2026-09-09T13:57:08.009Z" }, + { url = "https://files.pythonhosted.org/packages/6f/1a/cafb31049ecc1a6ce52bcc69fa436cca239adc057b1718a0c49044848663/multidict-6.8.0-cp315-cp315-win_amd64.whl", hash = "sha256:3bafff8598f0528017ddc74194e5451d5c22d046c98935f8f86247b0f286e4f8", size = 50294, upload-time = "2026-09-09T13:57:09.986Z" }, + { url = "https://files.pythonhosted.org/packages/6b/51/00e037da14cd1d894b123e0bbe62de5c561679a6ab23ab1c009f2965dcda/multidict-6.8.0-cp315-cp315-win_arm64.whl", hash = "sha256:e886ef8c9879105fe4fc99417447b3a5f35d1131412ce839470bd2089fe2043f", size = 47626, upload-time = "2026-09-09T13:57:11.738Z" }, + { url = "https://files.pythonhosted.org/packages/52/f7/aeb947982197e8b4f5c4da3961ee473ea5a050b94a6ff3b88baf64621401/multidict-6.8.0-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:883284137e25318ed9735b742ae46341a864888fae28e8b6314c4f84da080f08", size = 88801, upload-time = "2026-09-09T13:57:13.957Z" }, + { url = "https://files.pythonhosted.org/packages/35/d8/593948c016c3f850e3cd56a4e0144151eb409d2b8690f0c0ce7f7d33dbea/multidict-6.8.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:ca52b9ec80851366197577154c862c4c4c7036ca76ae94cef5cb59c5cfeab944", size = 53376, upload-time = "2026-09-09T13:57:15.94Z" }, + { url = "https://files.pythonhosted.org/packages/58/b9/097a05bca533027c0477b6a90bf927dbbb4b23cc9090bbb37a2e972af8d5/multidict-6.8.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:91fa75d0a693832106d98f66c849f034f21c828d14437f1fb97d3784aab89e84", size = 51629, upload-time = "2026-09-09T13:57:17.685Z" }, + { url = "https://files.pythonhosted.org/packages/fe/07/938ed21967f12380d0b8861645fb65a942f3669e31d5163ed94d23103b61/multidict-6.8.0-cp315-cp315t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:307c1acd812fe897e7fbe10c6758822e8c04be4e7c60a9f54901cdf8b5ab8bc3", size = 261967, upload-time = "2026-09-09T13:57:19.752Z" }, + { url = "https://files.pythonhosted.org/packages/89/e8/e66bf843fd29c01712dde9edeb9f4ad0ffab06ab4ada4b721ad7bc73b3d5/multidict-6.8.0-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5caf684986a2490628f059a99dd107b566a2d34cf947f8eb8387e0500a1f90c5", size = 265923, upload-time = "2026-09-09T13:57:21.784Z" }, + { url = "https://files.pythonhosted.org/packages/8a/f8/e9be849b225af28a8eee2c6bfea23594a777c753fe97e2ff7e2180c8935a/multidict-6.8.0-cp315-cp315t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:77745725125d01fd613b6db043362aa7c6bfbfdb23d45dbfc3d92bf58160af62", size = 239380, upload-time = "2026-09-09T13:57:24.3Z" }, + { url = "https://files.pythonhosted.org/packages/ab/67/4dbad08f5081978c591afae9e836ec9ddae90e9e76be6d6ce10757483dc4/multidict-6.8.0-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8daafaa0b2eb43f76898ced78b1e0fb91b38c4fa50da516c18067f2a2d578c20", size = 271591, upload-time = "2026-09-09T13:57:26.611Z" }, + { url = "https://files.pythonhosted.org/packages/92/3f/e9c97222d7e104e54e556f118ec7d091ab41a0c10c630f2b97e5b43f5404/multidict-6.8.0-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c68e0c0649d17c2d0339e3674e86a4aeba4a7e6b21c1e394cf947a95433b31d0", size = 276091, upload-time = "2026-09-09T13:57:28.997Z" }, + { url = "https://files.pythonhosted.org/packages/26/ca/728e7ce05ac9c0303554e7162e74d91fe49e65bad7dfbb377f783dd32c0a/multidict-6.8.0-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d8a5ac357ac283490a8d1899b0383355fd1f8634b14ba0d59e4c0dd97db85556", size = 266493, upload-time = "2026-09-09T13:57:31.256Z" }, + { url = "https://files.pythonhosted.org/packages/8f/74/7c658d2769863af16fb7d7c6be50b29659892a06a632858863eee3a31842/multidict-6.8.0-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:46029e6e27a3ec0dc55b53f58df82d10f04c5e111f78248279b530bedad2c30a", size = 245302, upload-time = "2026-09-09T13:57:33.464Z" }, + { url = "https://files.pythonhosted.org/packages/2d/2c/d4350a20a0e8c66a447d694e8713438262665203fe826c3f4e385f052b72/multidict-6.8.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:8d1046b5427dcafe6e8a0e07527dd74f1ee694006160162f53f3a17f15aad3b4", size = 261016, upload-time = "2026-09-09T13:57:35.651Z" }, + { url = "https://files.pythonhosted.org/packages/1f/78/83df999c8beb72a012cfac42f2b833c4a48f8e836fd4407747b355a2430e/multidict-6.8.0-cp315-cp315t-musllinux_1_2_armv7l.whl", hash = "sha256:f1f4a220db6ed7c8fd16b6d644ffd1f082651693204daf3275e049fadc849e39", size = 255021, upload-time = "2026-09-09T13:57:37.758Z" }, + { url = "https://files.pythonhosted.org/packages/fb/13/f2c0a2dac6d91f74aa124f3e9f07ec497ceae5ed2df2753d249601cd7262/multidict-6.8.0-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:029897732a9c798737457e382bf84e8c64237eff224a90aea2639f4413c45e4e", size = 263066, upload-time = "2026-09-09T13:57:39.897Z" }, + { url = "https://files.pythonhosted.org/packages/59/1d/730008d4639ace731bbb1399e1ac13cbdf506f7d6fb861d75044ffb3994d/multidict-6.8.0-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:29be9fd289e9ab8f480996ea2f686e1654b80242033843cb11691688329423f1", size = 266510, upload-time = "2026-09-09T13:57:42.39Z" }, + { url = "https://files.pythonhosted.org/packages/ab/ca/bec67a5d206dc5748e50c93f6f71deec14305c3657cfe250c3887caf7839/multidict-6.8.0-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:29631224698de1e42abc8fa7658d830e0aed0029785144b5832b695da5adef2f", size = 239423, upload-time = "2026-09-09T13:57:44.304Z" }, + { url = "https://files.pythonhosted.org/packages/9e/db/5f153fe51fbac7d80f3bb8bd6fab8db8b6cd061e7a11371676dfed3712bc/multidict-6.8.0-cp315-cp315t-musllinux_1_2_s390x.whl", hash = "sha256:962f18c59a000f30b084ea2e6b8001521bb315efd4e5f10acf9fb36f366b7882", size = 266902, upload-time = "2026-09-09T13:57:46.297Z" }, + { url = "https://files.pythonhosted.org/packages/89/0f/9efca48a351551de4dc0c183f523109dbe87c645a4732d5f1c70b4880dca/multidict-6.8.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:c60e50bc5b07faac92fd3a20fa21cc8cf3e3f7204d2867b206c73293ebc19101", size = 260887, upload-time = "2026-09-09T13:57:48.268Z" }, + { url = "https://files.pythonhosted.org/packages/df/d8/bb879a62e0809448e53f6237e71670066ecf3bbc5896a7a6705b6628d86a/multidict-6.8.0-cp315-cp315t-win32.whl", hash = "sha256:fc5460940f50dff00731b4132366840ba9685286ea88ea104b661899084f3fea", size = 50533, upload-time = "2026-09-09T13:57:50.31Z" }, + { url = "https://files.pythonhosted.org/packages/fe/62/3e5308d8871636e4b9620e4b3acfcf2b5caf79b19d317690ec13f7fc8b57/multidict-6.8.0-cp315-cp315t-win_amd64.whl", hash = "sha256:b367c342327717d644db4c0ddb37ceb655c84822215ea0773a3a36911b74b71d", size = 55572, upload-time = "2026-09-09T13:57:52.301Z" }, + { url = "https://files.pythonhosted.org/packages/b9/cc/d3c10e10ee3bb7a7b4abbb3157306b2ce7e0018c9c2d16b32b468739d2b7/multidict-6.8.0-cp315-cp315t-win_arm64.whl", hash = "sha256:0c1c4debad7337627b86837abdf0237ca3cb3d7e17de7eab0177c263878546d4", size = 50322, upload-time = "2026-09-09T13:57:54.099Z" }, + { url = "https://files.pythonhosted.org/packages/b1/ee/be4e1a4b7a2b27f4fb6936510d4bebcb41b0562c946930ad26916e069cf9/multidict-6.8.0-py3-none-any.whl", hash = "sha256:75daa15ca16d6285eb2e104b2f05ee6f8d9836c68da3ce5c85f615a0450eed0e", size = 16297, upload-time = "2026-09-09T13:57:56.106Z" }, +] + +[[package]] +name = "numpy" +version = "2.4.6" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.12' and sys_platform == 'win32'", + "python_full_version < '3.12' and sys_platform == 'emscripten'", + "python_full_version < '3.12' and sys_platform != 'emscripten' and sys_platform != 'win32'", +] +sdist = { url = "https://files.pythonhosted.org/packages/d0/ad/fed0499ce6a338d2a03ebae59cd15093910c8875328855781952abf6c2fe/numpy-2.4.6.tar.gz", hash = "sha256:f3a3570c4a2a16746ac2c31a7c7c7b0c186b95ce902e33db6f28094ed7387dda", size = 20735807, upload-time = "2026-05-18T23:37:14.07Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/49/ec46835a70be8fa6446c495126ac84fdb28cb2558e1620ffb87a10c8b64c/numpy-2.4.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0280e0356c0829a18d9de1cb7eee50ec22ca639878d7240307ca0943d73cd2c4", size = 16969194, upload-time = "2026-05-18T23:33:13.503Z" }, + { url = "https://files.pythonhosted.org/packages/0e/0d/f5957185c0ee2f3e12f78715aa9e3b353fd83633316c8532b38faa37e3f6/numpy-2.4.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:110f8b71aacb688ec69062bb7f6938a0f8acb01b7c1c4beb453c65b6d234584d", size = 14964111, upload-time = "2026-05-18T23:33:17.795Z" }, + { url = "https://files.pythonhosted.org/packages/ad/40/40a40ee0ddf7ceb782c49af278894b686e586d65d8c1889c8b5da01a3d7d/numpy-2.4.6-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:4cfe66903cc32a9921a6733d96b19bb6abf310397581bbad89c228f5abaf0ee8", size = 5469159, upload-time = "2026-05-18T23:33:20.654Z" }, + { url = "https://files.pythonhosted.org/packages/63/13/f9a8046535cb21deae82f8d03de9617e08882d274fad2539630761888228/numpy-2.4.6-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:8155154c7c691289fe18f510b5d4657c68c67989f293f0535a91360392ff6538", size = 6798936, upload-time = "2026-05-18T23:33:22.987Z" }, + { url = "https://files.pythonhosted.org/packages/33/a8/6fa8c1a345a8c85dbb21932c447bee07c30a2c2a3f31e369c0a84b300147/numpy-2.4.6-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ab0a9c4ffb1a6d95ef519fe4247dba8eb6b18ad93999f76b7f657039acabd47", size = 15966692, upload-time = "2026-05-18T23:33:26.62Z" }, + { url = "https://files.pythonhosted.org/packages/02/03/74fe2a4cb3817d94d86402f2506554130a2f01414e299b5a843e5a8a957f/numpy-2.4.6-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:89cd468399cfd2504718f0ba50e410dca55a170b61a02ad92bb18c8a65186e93", size = 16918164, upload-time = "2026-05-18T23:33:29.955Z" }, + { url = "https://files.pythonhosted.org/packages/c5/80/3615be3313f7e7696609bc194b9f0101da809df79e859bdb84e0cd043f46/numpy-2.4.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c2d37ab77531417474168eb79d6d80b14f821a966818505d03013d0833edb7a8", size = 17322877, upload-time = "2026-05-18T23:33:34.724Z" }, + { url = "https://files.pythonhosted.org/packages/ca/ac/a691e0fe2675e370d0e08ff905adc49a1c8830e8cae03efe4477e92cd55d/numpy-2.4.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f407cb6b8e9d6d8c626bc73c945db1706035af8fd632295547bf1c9e46d092d6", size = 18651487, upload-time = "2026-05-18T23:33:38.217Z" }, + { url = "https://files.pythonhosted.org/packages/15/a7/9bc1cd626d7bf6869bfedf27b91b6ab5dd607758bf8e959d6fa80c6a59cb/numpy-2.4.6-cp311-cp311-win32.whl", hash = "sha256:ddea102b48f9e339f3948bf22040944184627a30fdf7f858667673b9c5f033c8", size = 6233945, upload-time = "2026-05-18T23:33:41.331Z" }, + { url = "https://files.pythonhosted.org/packages/c5/31/7fc6239c12bce7e931463251cca4426c465e1876ba3cc785402ef4dd8f4e/numpy-2.4.6-cp311-cp311-win_amd64.whl", hash = "sha256:1e254a00cdf42b1e4d5b3d68d33af63268d41340d8885df2ab6470f2e1500147", size = 12608406, upload-time = "2026-05-18T23:33:44.131Z" }, + { url = "https://files.pythonhosted.org/packages/27/83/140f85a466595a16382996a1bf06b2b54bcd597488921b0c9daaeeda72af/numpy-2.4.6-cp311-cp311-win_arm64.whl", hash = "sha256:ed9749eef4cbd126da3dc1d6bcb3a57f5eb7ac6a6484146bdbf743f552dfc577", size = 10479528, upload-time = "2026-05-18T23:33:50.725Z" }, + { url = "https://files.pythonhosted.org/packages/95/2a/3d7b5ac8aac24feaf9ad7ed58f45b0bbc06d37e4338ae84c9f2298b570f9/numpy-2.4.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:001fbb8e08d942dd57599e781f2472269ee7f2755fae407b4f67b2f0b17da3f1", size = 16689119, upload-time = "2026-05-18T23:33:54.065Z" }, + { url = "https://files.pythonhosted.org/packages/ea/12/92c4c131527599e8288d6918e888d88726f84d805d784b771f32408aeaef/numpy-2.4.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ebfb099f8dcf083deef3ac1ca4c1503f387cf76296fcb3816b66f5ecb5f54fdb", size = 14699246, upload-time = "2026-05-18T23:33:57.621Z" }, + { url = "https://files.pythonhosted.org/packages/ad/fe/c0a6b7b2ca128a8fb228575147073b660656734b8ebe4d76c8fd748dcc79/numpy-2.4.6-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:3213d622a0283a39a93d188f3cf72b26862df52fbb4ca3697f51705016523d41", size = 5204410, upload-time = "2026-05-18T23:34:00.302Z" }, + { url = "https://files.pythonhosted.org/packages/f3/d4/9770d14ba719432bb90a421bfd443872ed0f70f7264b64bec12ea363d5fd/numpy-2.4.6-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:357cc07a6d7b0b182ff02249616a03742827ebb1277546b5c7cd7f7620a45698", size = 6551240, upload-time = "2026-05-18T23:34:02.852Z" }, + { url = "https://files.pythonhosted.org/packages/c9/c6/50a46a6205feba2343f1d6d17438107c5dc491ed1c736e6ea68689fd906b/numpy-2.4.6-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f9fb9157b4ce2971008323afe46053787b526ef624fea915b261468a8421a0f", size = 15671012, upload-time = "2026-05-18T23:34:05.485Z" }, + { url = "https://files.pythonhosted.org/packages/99/60/14115e6364fa676c5397c2ad3004e527e9aa487abf5d0706ec81bbd08529/numpy-2.4.6-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:90f9849678c75fe7afa2d348ac842c168b0a4d3d61919687216dfc547976d853", size = 16645538, upload-time = "2026-05-18T23:34:09.265Z" }, + { url = "https://files.pythonhosted.org/packages/ae/c5/693cbe59e57db94d2231fa519ca3978dc9e19da5a8f088588f5c6e947ff2/numpy-2.4.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c1a2af6c6ef86344a6b0db6b97834208bf598db514f2b155042439b62605601a", size = 17020706, upload-time = "2026-05-18T23:34:13.053Z" }, + { url = "https://files.pythonhosted.org/packages/ef/fc/85b7c4eff9b4966ade25c2273cf7e7012e92366c032058653934b37de044/numpy-2.4.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e5805d5a22fd19c8ccff10a9561f9df94436b0545619ea579db2d3c35294bce2", size = 18368541, upload-time = "2026-05-18T23:34:17.024Z" }, + { url = "https://files.pythonhosted.org/packages/f6/81/e1b27545deedce7f4a0b348618c6b62d74e36a4dc9ccd42f3eb2f85eee32/numpy-2.4.6-cp312-cp312-win32.whl", hash = "sha256:e3eeb0aabd6bd5ce64faae67e9935203a6991b4bc2a485a767fbafb2c5125f45", size = 5962825, upload-time = "2026-05-18T23:34:20.3Z" }, + { url = "https://files.pythonhosted.org/packages/ab/ca/feab00bd44aa5fe1ad2c18f08b4d3bb92e26484b0b1d1443897809ed528c/numpy-2.4.6-cp312-cp312-win_amd64.whl", hash = "sha256:d8e8286dd7cea7895157318d1b91cdacac64c479f3cbc8dce548331728484751", size = 12321687, upload-time = "2026-05-18T23:34:23.095Z" }, + { url = "https://files.pythonhosted.org/packages/63/cf/5a6d34850a39d1093558564f77ee8e8e0bee5061151b8f05a55711001ec7/numpy-2.4.6-cp312-cp312-win_arm64.whl", hash = "sha256:4081eb135ac24158bd51cdfbef16f1c64df7063b1143f24731387137c092bec8", size = 10221482, upload-time = "2026-05-18T23:34:25.876Z" }, + { url = "https://files.pythonhosted.org/packages/fb/82/bdab26d7438c6791ca31b7c024ca37c1eab8b726ba236129005cd4a06e45/numpy-2.4.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:511dbaf848decaaaf4b4ca48032619fb3138710c4bf7da7617765edad1ef96b0", size = 16684648, upload-time = "2026-05-18T23:34:29.41Z" }, + { url = "https://files.pythonhosted.org/packages/1b/30/a80189bcc7f5e4258b3fbc3968d909d1756f54d023299ecc39ad6fdb9ef8/numpy-2.4.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bf162abab1c1a736333192707cef898e735a5ca00f38f27eeedf44b39d9e85eb", size = 14693902, upload-time = "2026-05-18T23:34:33.013Z" }, + { url = "https://files.pythonhosted.org/packages/97/12/70b5d0d7c15e1ebb8a6a84a8caa1d19e181d84fb58bb6d70aca29099dec1/numpy-2.4.6-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:043191bfa8eab18c776647b62723ac9dddece59743b13f49b2016094129c2b3f", size = 5198992, upload-time = "2026-05-18T23:34:36.132Z" }, + { url = "https://files.pythonhosted.org/packages/ba/8c/ebd2a8f8a83541f8d38cc5667e8c2b69cecfd30da6e45693e8158857d44b/numpy-2.4.6-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:6180d8b35af935aed8ece3a85e0a43f87393ae0ac87c8d2c8bd2c993f7270ef3", size = 6546944, upload-time = "2026-05-18T23:34:38.484Z" }, + { url = "https://files.pythonhosted.org/packages/bb/c5/7b863a97a91671a0338f4253bd3b5a3d3852f0692dae91711c9f4a10e787/numpy-2.4.6-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:72fbe16c6fac95aedf5937fa873445cec2110be35d8a4e9433d7501fd98dae6b", size = 15669392, upload-time = "2026-05-18T23:34:41.257Z" }, + { url = "https://files.pythonhosted.org/packages/a5/9d/3584b9984ca4c047aea75214ce1a4c4c73d849bd71b604264b7f5653f8a8/numpy-2.4.6-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a7830bab239b79cda9c08c2da014761cafb48da6150e1da17ac06283f43b6089", size = 16633220, upload-time = "2026-05-18T23:34:45.075Z" }, + { url = "https://files.pythonhosted.org/packages/05/ae/7c67fba23bd98caec7c99261f3a16072ade14813486b0282cb29846de832/numpy-2.4.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ef4aea96ce4d3b074422cb4f2f64e216bf9e213004bb58ecfdf50ea02ea8eb9a", size = 17020800, upload-time = "2026-05-18T23:34:49.065Z" }, + { url = "https://files.pythonhosted.org/packages/d9/5d/3b6725cb31d983c5e66916f5d36f6d7e5521129e4c4404d64f918292a5b6/numpy-2.4.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:dfa20cc6ca228e6b155b11da03825975ce66aea520985dbbddf0f2a5a495c605", size = 18357600, upload-time = "2026-05-18T23:34:52.709Z" }, + { url = "https://files.pythonhosted.org/packages/f7/da/2ccc6c2fe8898dee01d90c75c5f5f914a23daf99e3e0f59516a08760c8b5/numpy-2.4.6-cp313-cp313-win32.whl", hash = "sha256:56b39e5e0622a09a25bf5baf62f4bcf0cb8a41ae6e2819cf49bbc5a74c083f91", size = 5961134, upload-time = "2026-05-18T23:34:55.618Z" }, + { url = "https://files.pythonhosted.org/packages/b5/cd/9cc4dc876fb065d5c220aae4d5e14826b2715331bb7618ce1fb07a679d99/numpy-2.4.6-cp313-cp313-win_amd64.whl", hash = "sha256:c4fc99836233ea196540b17ab0983aff60ed07941751930f5f4d05bc3b3b7359", size = 12318598, upload-time = "2026-05-18T23:34:58.928Z" }, + { url = "https://files.pythonhosted.org/packages/39/1e/c0bcba1f8694116485fe28fd1be698c278fcda4141c5b0e53a2aed8b12a8/numpy-2.4.6-cp313-cp313-win_arm64.whl", hash = "sha256:a7c711e21628b52034bb5ab8d1bce291f752fcc5e92accc615778acee1ff4778", size = 10222272, upload-time = "2026-05-18T23:35:02.167Z" }, + { url = "https://files.pythonhosted.org/packages/63/6d/cc5619247c8f4204e507f5883528372e4ac4bb189e579fb859a12e480b1f/numpy-2.4.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:112b06a867b235ef466ed3508ddf0238050df9c727cafb5301ac385b899189a1", size = 14821197, upload-time = "2026-05-18T23:35:05.468Z" }, + { url = "https://files.pythonhosted.org/packages/00/58/f1c39161c87d9e9bed660f1ed4bafc0e403d5ec9650b6dd77aead07d489b/numpy-2.4.6-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:eaf7fa2de5c0be8ae6ff8e9bea2ccd725e980541244521d8d4b5f3354a27babe", size = 5326287, upload-time = "2026-05-18T23:35:08.693Z" }, + { url = "https://files.pythonhosted.org/packages/af/57/3917ab0fd97f271a8694513581b8a36c655f111c446852c302f04ccdb6fc/numpy-2.4.6-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:7265a2f3d436e54ef9f2b52b5c937e6be778781bd97a590319d7348f1c1ca997", size = 6646763, upload-time = "2026-05-18T23:35:11.459Z" }, + { url = "https://files.pythonhosted.org/packages/eb/0f/037e64c494b67581ae18193d770adef354c41f3f2c8ebf865602d949bf8f/numpy-2.4.6-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f74a575920ab21fe304421a3fc28793d82e299cae9eccb37084e9fc7f3617c20", size = 15728070, upload-time = "2026-05-18T23:35:14.79Z" }, + { url = "https://files.pythonhosted.org/packages/21/a6/5d2bae9c9542eb4df16dc9c46dc79c186e9bad53805dfa5399a6023c6db0/numpy-2.4.6-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ede83e07a75dd06bc501566c1eca2afc0d61677c1472ac9ad93fdee6e638a48d", size = 16681752, upload-time = "2026-05-18T23:35:18.836Z" }, + { url = "https://files.pythonhosted.org/packages/92/14/23d1dfb410ae362cd59ce53e936b1513d545eb40db3949ced632e19a459e/numpy-2.4.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:68bb27509ac1b9a3443094260f6326150663b06abe40b73a2f81160623da5b67", size = 17086024, upload-time = "2026-05-18T23:35:22.52Z" }, + { url = "https://files.pythonhosted.org/packages/4b/6e/23595a2c642cdf3bc567877064bdd7f91c8b0038a4453cf2daf7248eafe9/numpy-2.4.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:a0df0043bdb289bde1f62da130d20df23d58b45429f752bc7a8fc5325a225ecd", size = 18403398, upload-time = "2026-05-18T23:35:26.398Z" }, + { url = "https://files.pythonhosted.org/packages/8a/90/0ac3bc947217e66dec77e7cbc6a1979d1af70b6461b82f620d3bccd5e4c8/numpy-2.4.6-cp313-cp313t-win32.whl", hash = "sha256:29a287e0cf63ff528da061de6b9f64a4618da591ca1046aafc54062e40ca7eab", size = 6084971, upload-time = "2026-05-18T23:35:29.387Z" }, + { url = "https://files.pythonhosted.org/packages/77/71/5673e351671a1d2bd6063b91b44f70c0affea7d1516fa7a6572941ba4aa1/numpy-2.4.6-cp313-cp313t-win_amd64.whl", hash = "sha256:25c692919ac5a01f170a3bfcd62d745b24fd095c353d50812637d6fcab442e75", size = 12458532, upload-time = "2026-05-18T23:35:32.175Z" }, + { url = "https://files.pythonhosted.org/packages/3f/88/19d3503c5046e688f049274b27a3ef3d771152fa80d3ba3d01a3dff61abe/numpy-2.4.6-cp313-cp313t-win_arm64.whl", hash = "sha256:1e978ec1e8bd0e0e4de6bb75de9d30cbb74db6b6a2bb727618613703ca0167dd", size = 10291881, upload-time = "2026-05-18T23:35:35.465Z" }, + { url = "https://files.pythonhosted.org/packages/f8/91/3ab2044d05fd16d343c5ac2e69b127f1b2854040dd20b193257c78028bd3/numpy-2.4.6-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:06ca2f61ec4385a07a6977c55ba998a4466c123642b4a32694d3128fce18c079", size = 16683458, upload-time = "2026-05-18T23:35:38.353Z" }, + { url = "https://files.pythonhosted.org/packages/8e/62/764ce66fa4147ae6d73071a3abf804ffe606f174618697c571acdf26a7c9/numpy-2.4.6-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:38efbc8de75c7a0fc1ac190162d892787f3f47b57cc291231aafee36b80982b7", size = 14704559, upload-time = "2026-05-18T23:35:42.14Z" }, + { url = "https://files.pythonhosted.org/packages/60/61/23f27c172f022e04025b7dc2367f4d63c1a398120607ec896228649a6f48/numpy-2.4.6-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:d581b735e177fdcdce6fed8e7e8880a3fb6ee4e3653a3ac6af01c6f4c03effc5", size = 5209716, upload-time = "2026-05-18T23:35:45.377Z" }, + { url = "https://files.pythonhosted.org/packages/03/71/21cf70dc6ea3e3acb95fc53a265b2fc248b981f0194ceb5b475271b8809d/numpy-2.4.6-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:0a041d3d761dc3c35cc56ce0351506a02bcbc25f7b169f652435141a17db9096", size = 6543947, upload-time = "2026-05-18T23:35:47.926Z" }, + { url = "https://files.pythonhosted.org/packages/d5/91/64288395ee1799bd2e0b04a305dce9666da90c961e1f3fe982a05ee1c036/numpy-2.4.6-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:40fdc1ae7125e518ea98e53e69a4ebc27e1fd50510c47b7ea130cf21e5e1d42b", size = 15685197, upload-time = "2026-05-18T23:35:50.863Z" }, + { url = "https://files.pythonhosted.org/packages/f3/eb/ebffaa97dc55502df69584a8f0dcf07f69a3e0b3e2323670a2722db9aa39/numpy-2.4.6-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a2c306dea656c12c68f51f4cea133cbe78ca7435eb28c735eac1d3ebe73be6e8", size = 16638245, upload-time = "2026-05-18T23:35:54.752Z" }, + { url = "https://files.pythonhosted.org/packages/b8/0b/54f9da33128d7e350fab89c7455902eeae70349ee52bddb448dc4a576f45/numpy-2.4.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:33111801a01c12a8a1e3721f0a9232f8cfc8ae2c6b7098167e6f623c6073f402", size = 17036587, upload-time = "2026-05-18T23:35:58.355Z" }, + { url = "https://files.pythonhosted.org/packages/b6/f0/fdebc1052db1cc37c64beb22072d67cd6d1c71adca1299f53dec2b5e20d3/numpy-2.4.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ae506e6902902557576a26ff33eda8695e7ecb3cb36c3b573a0765dee114ebdb", size = 18363226, upload-time = "2026-05-18T23:36:02.845Z" }, + { url = "https://files.pythonhosted.org/packages/aa/b4/298628d98c72b57e57f7165ae6a481a1deaf6f3c28262a6e4c739c275930/numpy-2.4.6-cp314-cp314-win32.whl", hash = "sha256:aaf159caa35993cb1f56fb9b8e4610d35758e7ca005412eb1daa856a78c9c4b1", size = 6010196, upload-time = "2026-05-18T23:36:05.92Z" }, + { url = "https://files.pythonhosted.org/packages/df/ac/46de6dda46478f7942f839e094970be2d4a861e005c4b3bf07c92e291a09/numpy-2.4.6-cp314-cp314-win_amd64.whl", hash = "sha256:b507f5c4c1d508876d1819b6bf9a49d365b96320b5d4993426b33a23ca4b8261", size = 12450334, upload-time = "2026-05-18T23:36:09.107Z" }, + { url = "https://files.pythonhosted.org/packages/78/92/b8b798ac784102c0da830d2257d59358e3d3d90d1e2b3f2575dad976c5cf/numpy-2.4.6-cp314-cp314-win_arm64.whl", hash = "sha256:6f41ae150c4e32db4f3310cdaf64b1593a03dbabe29eec77fc9b50fe64061df6", size = 10495678, upload-time = "2026-05-18T23:36:12.766Z" }, + { url = "https://files.pythonhosted.org/packages/30/34/ec28d1aa8115971537c01469ab2011ee96827930f0a124de1000cc2a7ed7/numpy-2.4.6-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ece3d2cfe132e7d51f44a832b303895e6f2d499c5e74dfbdb06ee246147a304a", size = 14823672, upload-time = "2026-05-18T23:36:16.473Z" }, + { url = "https://files.pythonhosted.org/packages/16/bd/f6d1fede4e54e8042a7ff97bb495510f3c220f94bcd9e8b228e87c92cc0d/numpy-2.4.6-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:e3e5193ef5a3dc73bceee50f7fdc2c90dbb76c42df8d8fae3d1067a583df579e", size = 5328731, upload-time = "2026-05-18T23:36:19.767Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f0/e105b9e2fd728a9910103884decd6951d9dd73896b914a98d9a231de02ee/numpy-2.4.6-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:17f9ade344e7d9b464a084d69bcf18fc691cb1db67c62ed80820bf4926d78f0e", size = 6649805, upload-time = "2026-05-18T23:36:22.266Z" }, + { url = "https://files.pythonhosted.org/packages/82/dd/1206a7ca6ab15e3f02069707ca96222e202af681bb73756da7527f3cb837/numpy-2.4.6-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9cd5ffd25db4e7ba6a375693b3fc0fc1791ec636c17db3720da19bde7180ec43", size = 15730496, upload-time = "2026-05-18T23:36:25.713Z" }, + { url = "https://files.pythonhosted.org/packages/51/e7/38d3ea825dcab85a591734decb2f6c67caa7c8367d374df1a1c3842f9b07/numpy-2.4.6-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7d92c3819208a60205a12a245c91ad70cb0a85336659b19b834205573ac8456e", size = 16679616, upload-time = "2026-05-18T23:36:29.652Z" }, + { url = "https://files.pythonhosted.org/packages/93/b7/caabfdf53edf663e0b4eb74d7d405d83baef09eb5e83bcd32d601d72b93e/numpy-2.4.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e85b752a1e912b70eaad4fafbd4d1238007ab221de2009b9a2f5ae7461239895", size = 17085145, upload-time = "2026-05-18T23:36:33.449Z" }, + { url = "https://files.pythonhosted.org/packages/f9/45/68d7c33a6bcf3e5aa3bdbd57a367e6f615286dfd6482f97e8ffeb734306e/numpy-2.4.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:29cb7f67d10b479ff07c17d33e39f78c07f71c40ef30d63c153d340e96cd3fb4", size = 18403813, upload-time = "2026-05-18T23:36:37.369Z" }, + { url = "https://files.pythonhosted.org/packages/9c/50/0753655aa844c99cd9e018aacf76f130f1bd81d881bb74bc0aef5d73a8ba/numpy-2.4.6-cp314-cp314t-win32.whl", hash = "sha256:260a5d70215b61ab4fadf5c7baacd64821842975eea312125ed3c39a6391b063", size = 6156982, upload-time = "2026-05-18T23:36:40.817Z" }, + { url = "https://files.pythonhosted.org/packages/b2/d4/7c67becf668f973cb490cec3e98dfd799d866f9c989a54d355672cfa0db6/numpy-2.4.6-cp314-cp314t-win_amd64.whl", hash = "sha256:81a1cca95ed5bb92aa8b10dd2cdc9a0d3853a50fad926c28b5d7e8ea54389627", size = 12638908, upload-time = "2026-05-18T23:36:43.996Z" }, + { url = "https://files.pythonhosted.org/packages/43/bb/e1c71a4295b1b1d1393d50dbb4f2a36283c6859d9d3892e84f00ec5a91d5/numpy-2.4.6-cp314-cp314t-win_arm64.whl", hash = "sha256:0c9136e14ed34a9e343a31c533d78a9813a69a3148332bce5e9821cb2f996e66", size = 10565867, upload-time = "2026-05-18T23:36:47.114Z" }, + { url = "https://files.pythonhosted.org/packages/de/12/b422cc84439adc0d00de605bf4a308890ae5c26f2c71fbd73e5d08fbb0dd/numpy-2.4.6-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:55cced7c52e981362f708ad635198e97a752dfba412cc03c23bbf3bd8d5cd662", size = 16847511, upload-time = "2026-05-18T23:36:50.673Z" }, + { url = "https://files.pythonhosted.org/packages/44/53/f481bef68011740f8849418d82db07230e825013f31f4eef5ba5b805316a/numpy-2.4.6-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d6da64deb6b8ed903e7560180a92f2d804ee1ba5eeb849ac2748b8c1aba1f6d7", size = 14889064, upload-time = "2026-05-18T23:36:53.879Z" }, + { url = "https://files.pythonhosted.org/packages/7f/57/42ed575c10ced8af951d426bc4e1f8aff16fd851db33f067036215a7f860/numpy-2.4.6-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:68a5124b13fa6cc2086764a20005d30bc0548146f7f5322f02fce212ca14317f", size = 5394157, upload-time = "2026-05-18T23:36:57.194Z" }, + { url = "https://files.pythonhosted.org/packages/6a/ef/f66cc724fcc36c1e364c67f51ae9146090b8b584f27d58b97fdae3edd737/numpy-2.4.6-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:948424b06129ce883307e8cff868c31396d8dc7630a59c61d70d98dbe70f222c", size = 6708728, upload-time = "2026-05-18T23:36:59.575Z" }, + { url = "https://files.pythonhosted.org/packages/1a/9c/c531f2293b91265d8b48e9b329f54fdd7ffae73cb4134ea10cca4237e9cc/numpy-2.4.6-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5dbbdb29840ca3d91ee0fece42fc29278886d908280bfec0a5846c6f901a3eb0", size = 15798374, upload-time = "2026-05-18T23:37:02.674Z" }, + { url = "https://files.pythonhosted.org/packages/1a/b0/413077f6b1153ed3cba361401c6783bbad6114804a000cc22eb71c13e190/numpy-2.4.6-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8ad03c0965fb3c692200e74d458ca28c1dbb4ce96f9a479a8aa041ad5fabca02", size = 16747286, upload-time = "2026-05-18T23:37:06.327Z" }, + { url = "https://files.pythonhosted.org/packages/15/ce/e5ec180bc41812edcd8daeb8639d205622c0e8c02259d8ab25a0201b3c2a/numpy-2.4.6-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:2803abfebfc990042cd494d8ce2d5f82e9d847af6d35ec486923aa19dbad5e73", size = 12504263, upload-time = "2026-05-18T23:37:09.715Z" }, +] + +[[package]] +name = "numpy" +version = "2.5.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'win32'", + "python_full_version >= '3.14' and sys_platform == 'emscripten'", + "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'win32'", + "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'", + "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", +] +sdist = { url = "https://files.pythonhosted.org/packages/13/01/11703282db468b85f6f7b8c7f22d058de5970d5c7e60a3a8aaa313c3de36/numpy-2.5.3.tar.gz", hash = "sha256:df2d5874ff183595a4ba404edd04f6bd9b5505c1d7708573f6a6c17489a67563", size = 20791231, upload-time = "2026-09-06T16:27:47.073Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d6/50/8fdbb16af64895706a45f06a4068e29db732ec180f3c1375f14123359138/numpy-2.5.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:cb189f09db39283b26bfd061ec16189e14f71c6755207f72a0f7540867afe5b9", size = 16994982, upload-time = "2026-09-06T16:24:29.244Z" }, + { url = "https://files.pythonhosted.org/packages/60/39/789131c1188c078dcb3a1692e72e1e050c68b88ffe72c9ccaac9bcd7a9cd/numpy-2.5.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f59a878c33d6b88122d80d239bb3b845d58708750b0cb06a09aebb9b18ec696c", size = 12009327, upload-time = "2026-09-06T16:24:32.491Z" }, + { url = "https://files.pythonhosted.org/packages/9c/59/a312e95696e5f601914dd8b6dd844692ba61670807417e24b68e337b5c70/numpy-2.5.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:a72f874bc9e10e4b8f80426fb49716d5141f64442a0c8418065093ec8017fbb0", size = 5445405, upload-time = "2026-09-06T16:24:35.071Z" }, + { url = "https://files.pythonhosted.org/packages/30/d0/5623a1707ed4fe16e3909fe3cf5ee3da004ae677ad23d83bbf3adf1a6faf/numpy-2.5.3-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:fc36dc566135b5eceec4cf89758fcb719266a019ef07dae1754ae7c9f617ef3e", size = 6783213, upload-time = "2026-09-06T16:24:37.253Z" }, + { url = "https://files.pythonhosted.org/packages/f1/32/84146fc020ad3c25f805f70ab60da46fe3c540a21369754a7e4369754b6f/numpy-2.5.3-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:76c2c1e6bfa5c84adc6434dfbf013aa92096a7985221762c8f11fedfd20fff58", size = 15687872, upload-time = "2026-09-06T16:24:39.751Z" }, + { url = "https://files.pythonhosted.org/packages/65/af/aa78d1a88805456e212b65461354cd943197fb9acecc4c90fd12295123a3/numpy-2.5.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b7e18c623bb5c95acb3b3328861272816ba199fb531921c5d6d0b675f1fde9e3", size = 16717410, upload-time = "2026-09-06T16:24:42.745Z" }, + { url = "https://files.pythonhosted.org/packages/3b/24/faa79d865e69a97ba17473b23a1b74094b2259c03e820c70297293b9ea49/numpy-2.5.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4f8929ee6c96bfbd7b4ed2032e0c03af86fe1826740ab61ddabf9072d06e57ff", size = 17040975, upload-time = "2026-09-06T16:24:45.961Z" }, + { url = "https://files.pythonhosted.org/packages/62/4a/8877e629445a7176297dffcaf9c485faa96a95d81728a62521ad55bd4c0f/numpy-2.5.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b5d93cf48f687479941d12b69c873ad2cc76bbd487f0091c2200636497f34034", size = 18476479, upload-time = "2026-09-06T16:24:49.35Z" }, + { url = "https://files.pythonhosted.org/packages/c8/db/35e1c2d38b04cbd5b731f9d71495e055e813197669d22b612f11748d2ff9/numpy-2.5.3-cp312-cp312-win32.whl", hash = "sha256:bf63afbe037eb5d2fe87fbcc7778e61da53ebaf21d938a4515aa73b62532a5d4", size = 6133378, upload-time = "2026-09-06T16:24:51.915Z" }, + { url = "https://files.pythonhosted.org/packages/3c/a1/accf6d4f0c80c5d9ba9735d6b1550e444180599f34dec69ca01360f717ad/numpy-2.5.3-cp312-cp312-win_amd64.whl", hash = "sha256:0a59a421a32580a009e8a1751345bf829631b990dc1794b80514ab722b435def", size = 12567828, upload-time = "2026-09-06T16:24:54.255Z" }, + { url = "https://files.pythonhosted.org/packages/22/43/1764aff32e4652526ae2f71fa8b3efd8d25c8a3d6926914454e47138ed1e/numpy-2.5.3-cp312-cp312-win_arm64.whl", hash = "sha256:ccb32e0525d29e8b0572eb84c9a57af0e7a4e615726927506f55063c62414034", size = 10485432, upload-time = "2026-09-06T16:24:57.278Z" }, + { url = "https://files.pythonhosted.org/packages/79/e5/8fb89cd46d14e35699d13bf943a5f5f441ecee8667120a1f6105ab89e349/numpy-2.5.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:66a78fe4556c60aceda5916f9eacd638b18e9e681016ec302dcb4682d6d4d034", size = 16991061, upload-time = "2026-09-06T16:25:00.411Z" }, + { url = "https://files.pythonhosted.org/packages/2f/06/9dc9e48b5e5e941c8b10350c5ff2d721da42a20517d911d15544246775ff/numpy-2.5.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:92f30e89b8ee0ecf363033576c422b2f58fed6a80bed0aa48dff6d14c654663e", size = 12003676, upload-time = "2026-09-06T16:25:03.475Z" }, + { url = "https://files.pythonhosted.org/packages/ab/2a/98282aa5b8f58b1157d440bb6282eed47e3632a5de53a714fbab17e659fe/numpy-2.5.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:f9a2353b37a1a9e78fd82b27ad7e2a32a2d036604d18f02b05e3136c62ca3b09", size = 5439695, upload-time = "2026-09-06T16:25:05.978Z" }, + { url = "https://files.pythonhosted.org/packages/a1/f9/b6533d777be9d6ffd29dc1be0867e563e6e8cc9a220ff1b716adc317f060/numpy-2.5.3-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:ccbc4665079665c3cf3bab4db9f6b095370cd6437d66be549b6c2a1fd19e1958", size = 6779395, upload-time = "2026-09-06T16:25:08.599Z" }, + { url = "https://files.pythonhosted.org/packages/73/85/735720d04ec197c5dcfacdfc9922667c7f1f5f496a279b7ba4d7c74c4cc7/numpy-2.5.3-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c76d5dde9f445058f83d0c02af00557a4db91de9a9a57c0df87d1535001d654b", size = 15681750, upload-time = "2026-09-06T16:25:11.173Z" }, + { url = "https://files.pythonhosted.org/packages/3a/1b/3b16a9bc514a440a7a0883684111dcb1ef1aee960af2ca95da8fc775f124/numpy-2.5.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a5fa86b80fd24bcd1aff83ad23be44ea323de3f787be8f8b15d4a65621e25321", size = 16708577, upload-time = "2026-09-06T16:25:14.171Z" }, + { url = "https://files.pythonhosted.org/packages/69/c4/386f397831b07328b639c96c5b62719346cf4baf07c68d927239752b1534/numpy-2.5.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bd4cb9ad3c7889b9b3fe0a9a9fb5d2ed26f9879bff2608d9f01aed147a20d231", size = 17042047, upload-time = "2026-09-06T16:25:17.582Z" }, + { url = "https://files.pythonhosted.org/packages/5f/3e/a700ecbf36e85ae8328fd3b0e12eeddc22ed6358a64cb2bd913e0d195d65/numpy-2.5.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1302b90c0e52281681b2975adfe8a860cb7b12216a27b4b0b4207c44bf7bccf0", size = 18465724, upload-time = "2026-09-06T16:25:20.949Z" }, + { url = "https://files.pythonhosted.org/packages/41/ee/38e785e88a4045f6ad1d1f2808dcdfafdca48c760260c0587bf171e29fc9/numpy-2.5.3-cp313-cp313-win32.whl", hash = "sha256:1c80eabb4035ecf4ca9cd49cde8a9fdd69a729e63e6474887d1523ade7aa277f", size = 6129003, upload-time = "2026-09-06T16:25:23.664Z" }, + { url = "https://files.pythonhosted.org/packages/f3/ec/100f2b1794ede74a9b3d7ec6b9736927f56713414c1dfe19ab6c383494bf/numpy-2.5.3-cp313-cp313-win_amd64.whl", hash = "sha256:71cad2b2a7451ab79d8f5e71b453485b6775963d5cf794179144a7463fe6e8ec", size = 12560965, upload-time = "2026-09-06T16:25:26.602Z" }, + { url = "https://files.pythonhosted.org/packages/80/b1/7dc825ca94c12acebbce4c37caa5e198695eb31424bc579679f32b1bb49d/numpy-2.5.3-cp313-cp313-win_arm64.whl", hash = "sha256:8e4dd766076855b5ff7ea52fa5f07ce26286726e0f8bff446b7739d02e6ea204", size = 10482343, upload-time = "2026-09-06T16:25:29.772Z" }, + { url = "https://files.pythonhosted.org/packages/70/78/cf416f15dc29375a229d9dfebf8db6e313f291580b39fa1a568b6052bb07/numpy-2.5.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:350ba9783ce969cf9f7ce6e6a9a58e1a6e2a19ca025b7ee448c4db727706212a", size = 16998686, upload-time = "2026-09-06T16:25:33.171Z" }, + { url = "https://files.pythonhosted.org/packages/9e/59/abcc2d8def4fd60eec7d87f92d27c13448ffd9ab14339bcc63a0d7a2fdea/numpy-2.5.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:012e66aca395d795496446e52aeeb5866312a5d4d3f27da270e5a0b43f70dc5c", size = 12013862, upload-time = "2026-09-06T16:25:36.748Z" }, + { url = "https://files.pythonhosted.org/packages/94/75/4640d2d6e4b64a049e48425a82728a41ef4adb61332d2cba68055774878b/numpy-2.5.3-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:adc1ada2662f8a5f960b8a10d9986897e7499ef07e06d4cfe7197f8cce923c07", size = 5449793, upload-time = "2026-09-06T16:25:39.476Z" }, + { url = "https://files.pythonhosted.org/packages/96/cd/625b57ae33d4ca560f32cc0b47b4a5922146d9beb998ddf773900d440a73/numpy-2.5.3-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:54a115e5a73b8fc44f0cebef486365a1894b5c9760685d4558b72b7c3eb846e0", size = 6785176, upload-time = "2026-09-06T16:25:42.069Z" }, + { url = "https://files.pythonhosted.org/packages/9c/72/12918652e7912ef9751e8694c88820fcd1908e0618cb23f5f3caa6004b7b/numpy-2.5.3-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:be5a8381859b6da607c84f4f7d6847725f1cf1853ef8a2c9e115b7d58bef47dc", size = 15703377, upload-time = "2026-09-06T16:25:45.135Z" }, + { url = "https://files.pythonhosted.org/packages/45/8f/9beacf79ca7c650688ad0baa80931adb988fe6e6e5d5903c23cc3dbd70eb/numpy-2.5.3-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b0521d0f4aebb6e06189451025fa17a913287b13c03d5fe05c017333b654ea5b", size = 16711928, upload-time = "2026-09-06T16:25:48.461Z" }, + { url = "https://files.pythonhosted.org/packages/09/8d/41d0a56e1ac4c87495c897a211b1368691b7237aadabec8b3b8f3a74d48f/numpy-2.5.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9deb49575e5b0b94ed72c8a64ec4d033381adc27e9060ae842971f697ba96104", size = 17059507, upload-time = "2026-09-06T16:25:51.873Z" }, + { url = "https://files.pythonhosted.org/packages/08/1e/0dfbc5cc251d54e2af790f254d24ec38637fa97ec7d5d11de7ffed787098/numpy-2.5.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b00eefbcf0f292945c4b4dec2ae845389ef5bcdcd596e6e4328051db5b5ba694", size = 18471002, upload-time = "2026-09-06T16:25:55.233Z" }, + { url = "https://files.pythonhosted.org/packages/b5/2c/dfa40f6991f8185c8c30ffd023dfcbb11888e823cfab9557b920f3bb7bed/numpy-2.5.3-cp314-cp314-win32.whl", hash = "sha256:c2381f82999704f818e2c987a865050e285ec3621262c66d40f5a96c8f899f8e", size = 6180485, upload-time = "2026-09-06T16:25:58.157Z" }, + { url = "https://files.pythonhosted.org/packages/a4/73/d2c08231e4fde7e415501fd02c715d96e98599b2d8384445933944152984/numpy-2.5.3-cp314-cp314-win_amd64.whl", hash = "sha256:2c25dfa72943e4336ddb6b0ee4277b47a0c85bede0807530ec68103bf58e2c10", size = 12698179, upload-time = "2026-09-06T16:26:00.789Z" }, + { url = "https://files.pythonhosted.org/packages/5c/e9/dcdcc9b95cf5f49815055573aee1b11cfbf5299f38a180e437ded050810f/numpy-2.5.3-cp314-cp314-win_arm64.whl", hash = "sha256:15aa985ac73a8db02db7663381aa109510449d3819d37206caed27b33a65a8a6", size = 10769383, upload-time = "2026-09-06T16:26:04.011Z" }, + { url = "https://files.pythonhosted.org/packages/49/c4/af8bc08a7ef4e1529a7c0cf24969accce316b783999802089a581ec99272/numpy-2.5.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ac7bb1c52d445bd4f8f7f97fefe6abc3a084dc4d63df50d79b17fa2b78e89297", size = 12132668, upload-time = "2026-09-06T16:26:07.138Z" }, + { url = "https://files.pythonhosted.org/packages/c5/ae/0f15eb56d4ec5e13c1f7ff04ff407f997d1acbadb45d3e1f2e2645a8f43c/numpy-2.5.3-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:e6ab667ba76450084eb64013762c438ea76d9d29cc676dcd6c2e9892ba37f841", size = 5568580, upload-time = "2026-09-06T16:26:09.828Z" }, + { url = "https://files.pythonhosted.org/packages/23/fb/c72a8f25d4b6e96c354e7ab45ace3b27dc11e5d6a13b6c7d0cd6b08bf112/numpy-2.5.3-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:f7fabeb6cea87d65f3b926de33d03fb016cfdc29314c90974383b5582ae72891", size = 6882634, upload-time = "2026-09-06T16:26:12.524Z" }, + { url = "https://files.pythonhosted.org/packages/07/a9/968c90ed2ab15060c338e8137f1215b5a60756ae07328e0a60d1c6734df4/numpy-2.5.3-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1fb6f8fb9ff0b3a69f52c66ce397b0246583e9f28616231b0e32ca49259a5fa6", size = 15748923, upload-time = "2026-09-06T16:26:15.092Z" }, + { url = "https://files.pythonhosted.org/packages/59/08/9df04103947b95e3b6b1f2ed1a70521f325647a31b82da6a2aae3a485508/numpy-2.5.3-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:93e1f5447e2b1e479d7bd74701e84746b86450cff1fc368b132d195e2b8f8211", size = 16746748, upload-time = "2026-09-06T16:26:18.43Z" }, + { url = "https://files.pythonhosted.org/packages/41/a0/14c8d5fe5b53a334aabb653deb391c0fef49558f491880ea300ed6785224/numpy-2.5.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c00abe94c1a69d75d827dcf1c025b25c8a45d230b3bcd77a9020883a1b047653", size = 17111561, upload-time = "2026-09-06T16:26:22.113Z" }, + { url = "https://files.pythonhosted.org/packages/c4/a6/d7e96e42f01522e154c32489640f16dfc4f6181d165d05fc3bec8c2c4999/numpy-2.5.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:536f963710a4e63934d80ac0dc4f478804a83e9a84b6828018f25d09953ada33", size = 18513945, upload-time = "2026-09-06T16:26:25.401Z" }, + { url = "https://files.pythonhosted.org/packages/25/39/3453afb7119d0449ef11c886874120ff180e2c337760e0e2d88f70f1a945/numpy-2.5.3-cp314-cp314t-win32.whl", hash = "sha256:4c8a6d2ebce6305fd82fbefca827775437147052a976ee7c94b36a0c1b52ac6c", size = 6335421, upload-time = "2026-09-06T16:26:28.175Z" }, + { url = "https://files.pythonhosted.org/packages/99/01/22815d2b19a1a746b1d45205cffebb3fe511a18acb75fba6c88491fc9894/numpy-2.5.3-cp314-cp314t-win_amd64.whl", hash = "sha256:9a37475425b431b4d060f23b4f52cd2f3aef6bc7c654bd760adf0040eec9d435", size = 12896420, upload-time = "2026-09-06T16:26:31.265Z" }, + { url = "https://files.pythonhosted.org/packages/fa/ee/a7cbba67eeaff038dc29ca8b98a88396c8b0cc9c89d4924f4a27a5c9150b/numpy-2.5.3-cp314-cp314t-win_arm64.whl", hash = "sha256:2d8240cb4c16fd831074aa2b2cf9fc54664d826341d61c372245b96a74a49a9a", size = 10857177, upload-time = "2026-09-06T16:26:34.167Z" }, + { url = "https://files.pythonhosted.org/packages/45/56/78194492883ff5eec90423fe56a3a44b154da047d88a6307f629713c584f/numpy-2.5.3-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:a6391fafaba97500887132cd582abc6e19452b1ac775a47caa7b24490e152058", size = 16996531, upload-time = "2026-09-06T16:26:37.287Z" }, + { url = "https://files.pythonhosted.org/packages/11/39/dd55c0af90bbab564b09ae3b0aa60ec5c02b900fa4f1ba23440525c8b32d/numpy-2.5.3-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:09d5a423c71ad5feb5625844ad58050e35df43871004b52ac9c0ad44a56775be", size = 12012569, upload-time = "2026-09-06T16:26:40.707Z" }, + { url = "https://files.pythonhosted.org/packages/b6/51/04f67d32e4862b281b1cb84ceeaed3421189a84fb6fb51a391cd6d5009f7/numpy-2.5.3-cp315-cp315-macosx_14_0_arm64.whl", hash = "sha256:f9579f383d1bf9df80081e72760e84960a7fd4f88cf0c9e535a8597c9bb646f5", size = 5448498, upload-time = "2026-09-06T16:26:43.435Z" }, + { url = "https://files.pythonhosted.org/packages/a3/c9/25b4dc0dd1344ec26c7319e84fd4e9809d2b5628f4e12decd618036e5178/numpy-2.5.3-cp315-cp315-macosx_14_0_x86_64.whl", hash = "sha256:86bff898a431c0fb71f7610b75726e75a54d47b37edc9d537f48de63bb3c0b90", size = 6783026, upload-time = "2026-09-06T16:26:46.374Z" }, + { url = "https://files.pythonhosted.org/packages/fc/c7/29285be1e5232a6e7ee3268a33c85843f5a8ee93350c6465cddd66ebbf76/numpy-2.5.3-cp315-cp315-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1f3ed25271581281f2fccb1adcedfcde4c07362eec69189b50baf6f90e3ae159", size = 15697322, upload-time = "2026-09-06T16:26:49.415Z" }, + { url = "https://files.pythonhosted.org/packages/55/49/bbad5335fb4996a16881f853ff3e0ba582f01720e55c89b1c06b8fc42a90/numpy-2.5.3-cp315-cp315-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ffdc76bfcae6b255dff75202c5e7feaf95b40246bc0a17944facc1fecf9f79ab", size = 16708995, upload-time = "2026-09-06T16:26:53.127Z" }, + { url = "https://files.pythonhosted.org/packages/ef/e9/1df35483760b04a65ea44669f89dc64f30e5aca098b48ceb8b1310b0e0fe/numpy-2.5.3-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:116f96cadd935c6122e9228d676fe7ede19e741f5c8bb1c3cddbe0c51ccebea2", size = 17052508, upload-time = "2026-09-06T16:26:56.464Z" }, + { url = "https://files.pythonhosted.org/packages/b8/99/66e54da8265cc8be8a7382bf96edce17aaa2837d6f484432025932a3caa5/numpy-2.5.3-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:09ffa5d903faeaa5c4dd05009cf81c8bab9f2cb37c548b8d39b65b4cfa7c97f7", size = 18468224, upload-time = "2026-09-06T16:26:59.966Z" }, + { url = "https://files.pythonhosted.org/packages/01/bc/b5e90a91c115168d793dfd2ad9c69c438c2fe7a13a437e770bc5b078e732/numpy-2.5.3-cp315-cp315-win32.whl", hash = "sha256:e01c918ac3d48e18a927cf7b14a26a3e29ff2bdf2eacb976da0aecd6a43ed034", size = 6179919, upload-time = "2026-09-06T16:27:03.166Z" }, + { url = "https://files.pythonhosted.org/packages/37/ea/780748fd3985109075514ef8fc64cd25f943e40dde13a6d59141eb268fc8/numpy-2.5.3-cp315-cp315-win_amd64.whl", hash = "sha256:e931e4f499e0dc7ef29d269a8e5b35dd722e5d14be07df6240166ea7c6532fae", size = 12697656, upload-time = "2026-09-06T16:27:06.153Z" }, + { url = "https://files.pythonhosted.org/packages/b3/16/407be69a2a87c8cab64d95975a8977a426a29e138f07e276ec258f0fe4e5/numpy-2.5.3-cp315-cp315-win_arm64.whl", hash = "sha256:26e15e4aecd8617dfbaecb37d223e365d7b39411fba20454be2670a96aa74cb5", size = 10767601, upload-time = "2026-09-06T16:27:09.297Z" }, + { url = "https://files.pythonhosted.org/packages/44/bf/a97ffb01e41d50a32a9177aef942a4d0e389a3daf451d04e5f38ef6afb87/numpy-2.5.3-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:6cef4bb1706dfec49243c05d921eefb4e190d41e2528b30d8035ea1f36b4c24a", size = 17090092, upload-time = "2026-09-06T16:27:12.907Z" }, + { url = "https://files.pythonhosted.org/packages/d1/24/136c02f2c2af9a067a84d0c3aa10c99012c0476fa5066732fa4a4202557d/numpy-2.5.3-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:d1c89973648c85069c5046ad460f7b8a00218b29a2e42359ac8cc63e9ab94832", size = 12129429, upload-time = "2026-09-06T16:27:16.089Z" }, + { url = "https://files.pythonhosted.org/packages/fe/6c/b47582d6597789bf946d5efbeb6b9e56fd8bcbd5efc6fbf51dbe1ea31eb3/numpy-2.5.3-cp315-cp315t-macosx_14_0_arm64.whl", hash = "sha256:214045a5bf00113a146ab9ee9730c44501af6723cdf1f6830932f7b5ef2e7af0", size = 5565452, upload-time = "2026-09-06T16:27:19.868Z" }, + { url = "https://files.pythonhosted.org/packages/be/b4/ef3cc6da73774202d4deae16bb321fd8298a4e0561e3539f8c4be237d916/numpy-2.5.3-cp315-cp315t-macosx_14_0_x86_64.whl", hash = "sha256:8617bbfae4486cf99c9f899966699428d19da931d06ca94ad3da986c76e15997", size = 6876736, upload-time = "2026-09-06T16:27:22.232Z" }, + { url = "https://files.pythonhosted.org/packages/9e/24/e3813329498596cb842703dcacac1741612ed9fb9c4e6a3e0c7e2ebbc597/numpy-2.5.3-cp315-cp315t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:595d020938c84e320bcf40ad71089e108eac0d377cd018e14a8c094f39e98d85", size = 15745777, upload-time = "2026-09-06T16:27:25.181Z" }, + { url = "https://files.pythonhosted.org/packages/4a/9e/4e7a07fd0776dc2210cdacf2010be8665194d094defc10c419d7dea794cc/numpy-2.5.3-cp315-cp315t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f24021b9f22bc6301c37b196974a92c1c18dccedb6fef3dd252e95f2d6adbe4", size = 16746949, upload-time = "2026-09-06T16:27:28.576Z" }, + { url = "https://files.pythonhosted.org/packages/91/db/01674c0e20335057813a00c2ebd546ed25bff9ed7914f9bced00f8c55d94/numpy-2.5.3-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:71b39d9f935b6ec0f8753e3e2afb51e3efba6f2e05b68b32a40754d24bcd4a3c", size = 17108994, upload-time = "2026-09-06T16:27:31.946Z" }, + { url = "https://files.pythonhosted.org/packages/45/7a/584c5e71f8d378e57cac0b033891ed65c683ef90573ba4854e8c28203db0/numpy-2.5.3-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:6b05c171afb3aa07adbd20abc00aea86fe375beb0fdb9ef780ec5b7f63bab1c0", size = 18512266, upload-time = "2026-09-06T16:27:35.196Z" }, + { url = "https://files.pythonhosted.org/packages/a1/d2/4e1014173aa3c55e6a756e0e567290743a6ab33a288460374d7ef6bcd239/numpy-2.5.3-cp315-cp315t-win32.whl", hash = "sha256:f54660b0eb6b0b9f36e7fe1cdfdff472028dd0d14acd9b9b65098efbad059469", size = 6330292, upload-time = "2026-09-06T16:27:38.149Z" }, + { url = "https://files.pythonhosted.org/packages/6c/b0/ff5658a58199b7bcaad87bf260eef6713d9d42cca4e028f935b4fc5fbac6/numpy-2.5.3-cp315-cp315t-win_amd64.whl", hash = "sha256:1aad64d99730d013cfc6debafed22783b4fc5a7f4b8bc744d2d8cf7dcc880551", size = 12884918, upload-time = "2026-09-06T16:27:40.965Z" }, + { url = "https://files.pythonhosted.org/packages/fb/0b/b12a2df5d1b774bd9007a6fdff9381145b6223d37f11afc9c37ab0efd9a1/numpy-2.5.3-cp315-cp315t-win_arm64.whl", hash = "sha256:befa1ae5bd6030b3f512b43ff3fa5290bbed6b84411a44244b14adf835f5b89d", size = 10850807, upload-time = "2026-09-06T16:27:43.868Z" }, +] + +[[package]] +name = "opentelemetry-api" +version = "1.44.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ee/8b/aa9e2d8b8dfa7c946f7dec5d1f8f6ba8eca062f43509a06bdb5ce93d26c0/opentelemetry_api-1.44.0.tar.gz", hash = "sha256:67647e5e9566edcf421166fdf022b3537f818635daa852b289e34604dc6fb33a", size = 72406, upload-time = "2026-07-16T15:25:32.678Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ca/6f/a04e900f465ff3221ccc395522503e2d10e79fa21f2723c8e177aae1e0d1/opentelemetry_api-1.44.0-py3-none-any.whl", hash = "sha256:94b98c893a91b88657eaac1e3ba89618cdb85be6918196705354f34728b2cdef", size = 60018, upload-time = "2026-07-16T15:25:11.657Z" }, +] + +[[package]] +name = "opentelemetry-sdk" +version = "1.44.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-semantic-conventions" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5d/77/a6592cbc7c8d9bcc9d6757a9df45e04a7c585e3e6e7a13456da522b21109/opentelemetry_sdk-1.44.0.tar.gz", hash = "sha256:cebe7f65dc12f26ead75c6064de12fd2a9052e5060c0272d402cfa203aae123b", size = 208624, upload-time = "2026-07-16T15:25:46.078Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/23/ff077e61886ee020a17ce9c8b6fa11c601c8d8345b09ea24f605445df62a/opentelemetry_sdk-1.44.0-py3-none-any.whl", hash = "sha256:df081c4c6bcfdb1211e3e86140376792643128a25f8d72d1d27675936e7e96ad", size = 137221, upload-time = "2026-07-16T15:25:29.534Z" }, +] + +[[package]] +name = "opentelemetry-semantic-conventions" +version = "0.65b0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8f/73/0cbdebcb4cf545fdd328da14f5137e37d0770c3f26185e478b0d15d94f50/opentelemetry_semantic_conventions-0.65b0.tar.gz", hash = "sha256:f9b2b81e9d5b64f11bc952075e7e9c7fb0aab075c7fd1c46d597f1b919852d60", size = 148774, upload-time = "2026-07-16T15:25:46.902Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a6/0e/49df70d9b81fb5cbae4bbf2a49d865b09bcbcbc4eb53f5851b1027738d78/opentelemetry_semantic_conventions-0.65b0-py3-none-any.whl", hash = "sha256:1cacde7b0ad306f84c5ef08c3dbe1bbaf20165bba6f8bff43b670e555a086bcb", size = 204645, upload-time = "2026-07-16T15:25:30.688Z" }, +] + +[[package]] +name = "packaging" +version = "26.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/fa/3944b40b07da9ce895c0e6303a5ab7d53da063554f534556b134a54d6093/packaging-26.3.tar.gz", hash = "sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79", size = 313412, upload-time = "2026-08-04T18:15:28.737Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/63/34/ba1c580383c9eada3711951fef0795c80b829a078d72188184bcab9dd527/packaging-26.3-py3-none-any.whl", hash = "sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c", size = 129956, upload-time = "2026-08-04T18:15:27.159Z" }, +] + +[[package]] +name = "pandas" +version = "3.0.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, + { name = "numpy", version = "2.5.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "python-dateutil" }, + { name = "tzdata", marker = "sys_platform == 'emscripten' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/be/4f/5f3422a2afec5ffc46308b79e53291365a93748b498ac2e58bead0197916/pandas-3.0.5.tar.gz", hash = "sha256:dca3734d6ab7c906e6730f0788b0a1dbb9f2467731f9711f77995c8e9d62d712", size = 4658219, upload-time = "2026-07-22T22:19:28.819Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/48/ef/f1fd7431d635bf20015489bf0bd69c17fff1018de773540f651455a3916b/pandas-3.0.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2946e77e4a53cd248cbde631a12f0e51c8324ce354c3eba4d20147c1ad6f4282", size = 10397178, upload-time = "2026-07-22T22:17:48.274Z" }, + { url = "https://files.pythonhosted.org/packages/31/b4/0eafac990a431561187694126de01f9b12559549b4d86360c0c4bd870fde/pandas-3.0.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:71ecc8fb7ed1a7aa4392316b5309a6347e8e7f832f38fd897846b3a1457a9298", size = 9990736, upload-time = "2026-07-22T22:17:52.388Z" }, + { url = "https://files.pythonhosted.org/packages/de/21/359880af3ea9b7cb23bea5b51e8e70ef3866c03be09da9a2787e18e330a8/pandas-3.0.5-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b173f5951ff6b8b0ec7675e20dff3c97b7e7a57dfcce387c2d7c5afe87cb7899", size = 10814438, upload-time = "2026-07-22T22:17:54.708Z" }, + { url = "https://files.pythonhosted.org/packages/d1/50/d6cc4d7e508bbccf5d6027314a8312bc7ac73d0ec7f195f53838daafab40/pandas-3.0.5-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2c0cf1dd9b55a22d105fc46c1b489af3bd42264fcba7c66297bf47a9a1d9c78a", size = 11323634, upload-time = "2026-07-22T22:17:56.858Z" }, + { url = "https://files.pythonhosted.org/packages/70/2b/d5f0a8c90dd0ae04e64ba53b871afb796ec026b615086d382ddc2ade729b/pandas-3.0.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0fac0010c75e4efb6b99e249c183a8993ce0dc95c240f9b120a5e67c727b7928", size = 11850860, upload-time = "2026-07-22T22:17:59.1Z" }, + { url = "https://files.pythonhosted.org/packages/5c/30/183aec2e19adf778a98d29b5729a0a68f4cc4ebf9b9c3b70d0297355bcb1/pandas-3.0.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:08d24fe11a17dc33bd6e937dc9c665f9cba08fbdc9f657f405713515febe300d", size = 12411100, upload-time = "2026-07-22T22:18:01.485Z" }, + { url = "https://files.pythonhosted.org/packages/fa/9a/31f4983f191af51ab2a8f2d0c7b33dff3a84da26533f982fff02c2f9e28b/pandas-3.0.5-cp311-cp311-win_amd64.whl", hash = "sha256:b1261758dfb6cf12c3cff8300e21cefad30e7ec709abb4c24ac7318e6a52462a", size = 9968804, upload-time = "2026-07-22T22:18:03.903Z" }, + { url = "https://files.pythonhosted.org/packages/49/97/7886c89a39045c69ad82cbceaf3343810480c8ef49a216319ce8183860a6/pandas-3.0.5-cp311-cp311-win_arm64.whl", hash = "sha256:679f4e85b30ddb1515458ab1e788d3e260eae369b1f78da7a3aa4cac8ebf4a2a", size = 9205447, upload-time = "2026-07-22T22:18:06.134Z" }, + { url = "https://files.pythonhosted.org/packages/1c/54/1dc810ea558d1320b597aa140a514f2fdf1d2ea09c38cf556f13ea712ec9/pandas-3.0.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fa290c16964d4963fbfbc358928239cf3bd755b20e988ce944877def2f44471d", size = 10411717, upload-time = "2026-07-22T22:18:08.307Z" }, + { url = "https://files.pythonhosted.org/packages/68/56/fbe81c09195924d8b7b8d4461a20458fe80a6a5ed6b24f0314da684277e1/pandas-3.0.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c2e26bb46934b8a2ca0c3de1d3d606fc5f6746584791b2db264d58cf370e08dc", size = 9957095, upload-time = "2026-07-22T22:18:10.6Z" }, + { url = "https://files.pythonhosted.org/packages/e0/51/fac252f4a913ed5eabf3c11b880a9e8d5a6c10f0b2129d0462212d238b4d/pandas-3.0.5-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:73fa87b08a7ef706f8aafda39ddaccf2a99047bea62d8c88a0361bcafb2237bc", size = 10485458, upload-time = "2026-07-22T22:18:12.834Z" }, + { url = "https://files.pythonhosted.org/packages/12/98/e976540c1addf70442be7842a18cf70884a964abbf69442504f4d2939989/pandas-3.0.5-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d373ce03ffd84010ed9839fa73672a9c8256990532e158440c0085db7d914b34", size = 10998091, upload-time = "2026-07-22T22:18:15.209Z" }, + { url = "https://files.pythonhosted.org/packages/a4/8c/1f29b5be8d3fc47dd7567eb167fabba2085879b31e0287ce7cba6d3d2ff4/pandas-3.0.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2a29c53d85ea98c5e792c59ef82ee9fbe6ca902c0d0adb6b23f45ef894cd7bf6", size = 11499501, upload-time = "2026-07-22T22:18:17.689Z" }, + { url = "https://files.pythonhosted.org/packages/9d/e2/bd9c98ad2df7b38bde002adde4cdf353519da51881634323b126c55997f9/pandas-3.0.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a5ad3b02ed6bc7d7ae9b70804b2c6aa31827489d150f8e623ce82491b82085d7", size = 12060559, upload-time = "2026-07-22T22:18:20.147Z" }, + { url = "https://files.pythonhosted.org/packages/f3/9a/ffbd852d58bd74a617fe2f8ee6a58a96982271ce41cf981eab22190b4a4b/pandas-3.0.5-cp312-cp312-pyemscripten_2024_0_wasm32.whl", hash = "sha256:b2acb4650527eec6822c3dadb2b771277b65e7dae7a267d4bccf65fd1bb3fbce", size = 7197652, upload-time = "2026-07-22T22:18:22.502Z" }, + { url = "https://files.pythonhosted.org/packages/70/b5/d2d3e9ae73362ba4229651b0ee1455cf78073a1ce585f6ff693782ce263e/pandas-3.0.5-cp312-cp312-win_amd64.whl", hash = "sha256:80a611068e8a3ac23f7398c6c14eb46dc974e5cc9997f653e2dcfd1da74edd41", size = 9831691, upload-time = "2026-07-22T22:18:24.534Z" }, + { url = "https://files.pythonhosted.org/packages/52/51/dea1e89d6a6796b9c43f85a09b484ee03edb8a4c4842e73e200a8c11301c/pandas-3.0.5-cp312-cp312-win_arm64.whl", hash = "sha256:25ff585b972a18ef1fe9ffa3ac6544d9950508aa76832e5147640b6022821e49", size = 9105796, upload-time = "2026-07-22T22:18:27.064Z" }, + { url = "https://files.pythonhosted.org/packages/bf/09/7b95c4a0025227d6f118c4039b423412ac6a982db02864166185d812fbc7/pandas-3.0.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c1c05a767fe8e5b4fe9e1c29806829c582052eaedb9120a3da83ba3f69e24a5b", size = 10385742, upload-time = "2026-07-22T22:18:29.346Z" }, + { url = "https://files.pythonhosted.org/packages/8d/0c/dc78fd8c4da477b4b5e8ad37295af352190d21ef63a9ee1bc071753074cc/pandas-3.0.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b86765f268b56f7e665b93bce9d5df69dee7f99e595cf8fb839483ab315942a3", size = 9932067, upload-time = "2026-07-22T22:18:31.833Z" }, + { url = "https://files.pythonhosted.org/packages/3e/71/3592c055cf44df9808550f9368ceda80ff2b224d355ef73fe251dcda1802/pandas-3.0.5-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c597ecf5616b5c420372c1d4d4c00dbbfba7398bea857dcc984347e1ea48417b", size = 10466756, upload-time = "2026-07-22T22:18:34.195Z" }, + { url = "https://files.pythonhosted.org/packages/e3/70/4363150359f95b4cb4bcbb34ca23572bb5495749a621a8f3d5a1ddfd293c/pandas-3.0.5-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4b11c36e218331d0387cbe3a0a5f75162357a1d92d57b2b08a336ff94b19b2be", size = 10938525, upload-time = "2026-07-22T22:18:36.81Z" }, + { url = "https://files.pythonhosted.org/packages/f7/d0/317e7a0c67c0e69fa905a0161409397a7dc2d46ff611f6ca4803352c042b/pandas-3.0.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cf52e1f61d229496da17dc7ab54acdee627357e7008fd4fecba3d0ba2937fa58", size = 11489303, upload-time = "2026-07-22T22:18:39.287Z" }, + { url = "https://files.pythonhosted.org/packages/f1/8d/36dade89b49e4f9d5cbdbe863772581f98c0c6d78fc39ad4c557f6f2e17e/pandas-3.0.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:db172144bb56422bd157812f3b021eacc255451470b31e2c633c349490a1cfee", size = 11989004, upload-time = "2026-07-22T22:18:42.208Z" }, + { url = "https://files.pythonhosted.org/packages/9c/ba/18c4ec8a746e177da05a9e7a7963781d8ea195780724f854601b6ebd6b78/pandas-3.0.5-cp313-cp313-win_amd64.whl", hash = "sha256:0d298e951f23016ce4699951d044ae6418dbc91bf68cefca0f77666fcbb4e5c6", size = 9826896, upload-time = "2026-07-22T22:18:44.539Z" }, + { url = "https://files.pythonhosted.org/packages/de/ec/28a57266b753799a87b8bc79e7887ac6fd981b8c6d2978a0b7e7b6bd708c/pandas-3.0.5-cp313-cp313-win_arm64.whl", hash = "sha256:66266d3442a5e8b3c90274c2b8b230bee42dd1c286bc822cc2f9f2c7e12b883e", size = 9094790, upload-time = "2026-07-22T22:18:47.468Z" }, + { url = "https://files.pythonhosted.org/packages/51/2f/cf6aae281264f4463f0875bcbb15fd2bb6d291cc535187dad1732475e4a9/pandas-3.0.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2f264fc46911cc8131a7322a16199bbf8e353d27c10bb211f5bd0c814324dc36", size = 10390034, upload-time = "2026-07-22T22:18:49.818Z" }, + { url = "https://files.pythonhosted.org/packages/06/ec/5189518c7a7659c4bdcc6b1eb32c46c6f3c86b0661ffd84143d1112c7732/pandas-3.0.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:53730687fcd161883b24e10411c06d6a4c0f2275d2faf3bb2bc25deb4ba8007c", size = 9980065, upload-time = "2026-07-22T22:18:52.249Z" }, + { url = "https://files.pythonhosted.org/packages/ea/f1/598503ce8d7e3c35601e0747ba288c7864baae66380725bc12f13f884dfe/pandas-3.0.5-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:960d3ebcf249f75206899fcd2c6de53f736b7265759ced0d3e559df0b8b709b0", size = 10545532, upload-time = "2026-07-22T22:18:54.813Z" }, + { url = "https://files.pythonhosted.org/packages/fa/de/ceae2adf7034e07e9910299fe412e1819c4f0dd520700a888bcb03625448/pandas-3.0.5-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e94c2c5ca43bd3ca32bf64d32308887b65e5f9bfd8023ea52755107a999f93b", size = 10963120, upload-time = "2026-07-22T22:18:57.42Z" }, + { url = "https://files.pythonhosted.org/packages/66/25/86e0f4451874eb79e688deeebe3c451fec4557f8952005818d800ee8ac7e/pandas-3.0.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e819dd5f62966b481a8cb649d3299ebd886a1ea91ed5a99bf7ce77c98d18ab94", size = 11563178, upload-time = "2026-07-22T22:18:59.729Z" }, + { url = "https://files.pythonhosted.org/packages/f3/45/8643daa3b4147e433adfcccefdd0380d3aad79d86b15d8999730fe1944d5/pandas-3.0.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:3c5ed2e7c06e91d340dfd091d7934f9bc82e4a36b95f647f090b9d1c9ac649da", size = 12028708, upload-time = "2026-07-22T22:19:02.164Z" }, + { url = "https://files.pythonhosted.org/packages/96/58/ad979ae617615576e8aafd569c9d4b62f1191d896e38f51d66ba06f3b89a/pandas-3.0.5-cp314-cp314-win_amd64.whl", hash = "sha256:cd8f7c6dc98527058ee6264219343f5392240a6f1bfa654fc5d79023020d0c92", size = 9951806, upload-time = "2026-07-22T22:19:04.596Z" }, + { url = "https://files.pythonhosted.org/packages/69/32/7ac03886b304049a9d2625ee88f59af760d8a93bd30ed9239bce7b9869a8/pandas-3.0.5-cp314-cp314-win_arm64.whl", hash = "sha256:5183427f5a8156d480f30333777bc978be93650a49a7c01db26adffe95b31e85", size = 9238297, upload-time = "2026-07-22T22:19:06.836Z" }, + { url = "https://files.pythonhosted.org/packages/be/ed/1d1f2ee5547d5167face2376d11c8b2a4c7bfff5a416ee7a9046891fab1e/pandas-3.0.5-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:303da736987d481074ca720ada325f8bd80c64ebc2d45ed79b29df3aaa4a26ca", size = 10849690, upload-time = "2026-07-22T22:19:09.391Z" }, + { url = "https://files.pythonhosted.org/packages/57/55/17e17152e98fbb0c4b1e562bc65387a2f20a80db0f4a86bf8d3a0e4248d4/pandas-3.0.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3b2801bbb049d0136f6c213eae02b5fca969384fc2064dd728d8620552aa49da", size = 10509945, upload-time = "2026-07-22T22:19:11.773Z" }, + { url = "https://files.pythonhosted.org/packages/88/90/817d44dbf83facf9556f33576d9af0a241981e7bb5c00606c0bcb5df8dda/pandas-3.0.5-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cce3a9d11d2b1f82c69a27ec1f4948a170e2c403c4bbfa8cca62e3fdebe2ef3a", size = 10392197, upload-time = "2026-07-22T22:19:14.024Z" }, + { url = "https://files.pythonhosted.org/packages/f1/da/889f00c0a6f5aa1545add70abbf01502dff87ab577adb855bd631c54d2f2/pandas-3.0.5-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ef01af4d8dc6cd2c8d6c7736f149574ef93fe043811eeb5e445f2647154b5040", size = 10862726, upload-time = "2026-07-22T22:19:16.351Z" }, + { url = "https://files.pythonhosted.org/packages/bc/98/f1e934fb3c98fce859c6147c6785816c7b5b9ab7821115c5d8c4de9842b9/pandas-3.0.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e2759e890db96dfcffdbd9b86c3c2cb6afaf58def482820317e06163ec1066cd", size = 11414864, upload-time = "2026-07-22T22:19:18.981Z" }, + { url = "https://files.pythonhosted.org/packages/fe/be/d448af7d657d82e1888dd8551f79c6d6fb161080b5b9752d84d910ec2319/pandas-3.0.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b58b1b39d46a5862e3fb18f50d1a201398619d16a0f9f73f57eea5583cf0e63c", size = 11925105, upload-time = "2026-07-22T22:19:21.515Z" }, + { url = "https://files.pythonhosted.org/packages/29/c1/ccb4238212c8c4f496c584f3044d94e0c030ed8e1d68999db46c91c2242f/pandas-3.0.5-cp314-cp314t-win_amd64.whl", hash = "sha256:1c10461f6eeb35d8f05b6184c65c8b9991663b66c46b1d559b682cb34ae7c6ea", size = 10387612, upload-time = "2026-07-22T22:19:24.257Z" }, + { url = "https://files.pythonhosted.org/packages/d2/cf/6a51b2c38980e04c279fd2fa908a1b0982064e860444acfca4ec2e2c8359/pandas-3.0.5-cp314-cp314t-win_arm64.whl", hash = "sha256:3c5015fd1730fbf883647e88068176c839c102cea883ba1769a6f4593bfc1f8c", size = 9509776, upload-time = "2026-07-22T22:19:26.694Z" }, +] + +[[package]] +name = "pfzy" +version = "0.3.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d9/5a/32b50c077c86bfccc7bed4881c5a2b823518f5450a30e639db5d3711952e/pfzy-0.3.4.tar.gz", hash = "sha256:717ea765dd10b63618e7298b2d98efd819e0b30cd5905c9707223dceeb94b3f1", size = 8396, upload-time = "2022-01-28T02:26:17.946Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8c/d7/8ff98376b1acc4503253b685ea09981697385ce344d4e3935c2af49e044d/pfzy-0.3.4-py3-none-any.whl", hash = "sha256:5f50d5b2b3207fa72e7ec0ef08372ef652685470974a107d0d4999fc5a903a96", size = 8537, upload-time = "2022-01-28T02:26:16.047Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.11.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/53/18/f3bb8ef0d3b930692343da8aa4d3cbcd6749477c053959395ac81965a6e9/platformdirs-4.11.8.tar.gz", hash = "sha256:f23abafea7dd4276d1f29104b83598d7dcc567cafd07c9c951e66665645437fc", size = 37182, upload-time = "2026-09-08T22:20:42.866Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/e1/5b7b8bbb55084d1425bcb9bc823ff519e1b2be05f6ebb0089e2eacc38413/platformdirs-4.11.8-py3-none-any.whl", hash = "sha256:52f2f181bbfde907966932cc8312d967d02976422d66d537ea16092b8e291081", size = 24027, upload-time = "2026-09-08T22:20:41.537Z" }, +] + +[[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 = "annotated-types" -version = "0.8.0" +name = "ply" +version = "3.11" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/5f/56/a8120250d128bed162cd73c76d45f6ef9991f3e068f62a8ee060afa3104a/annotated_types-0.8.0.tar.gz", hash = "sha256:13b2beaad985e05e2d6407ee4c4f35590b11f8d693a258a561055cac8f64cab7", size = 15893, upload-time = "2026-07-23T20:16:13.995Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e5/69/882ee5c9d017149285cab114ebeab373308ef0f874fcdac9beb90e0ac4da/ply-3.11.tar.gz", hash = "sha256:00c7c1aaa88358b9c765b6d3000c6eec0ba42abca5351b095321aef446081da3", size = 159130, upload-time = "2018-02-15T19:01:31.097Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/99/91/8acff4f5e50511b911bbccb72b8628a49c68ce14148cd9f6431094859a90/annotated_types-0.8.0-py3-none-any.whl", hash = "sha256:f072f4d804ea359e4eaf198b1af7a8b0943881a87f31bb764f8bf219bb9419e0", size = 13427, upload-time = "2026-07-23T20:16:12.938Z" }, + { url = "https://files.pythonhosted.org/packages/a3/58/35da89ee790598a0700ea49b2a66594140f44dec458c07e8e3d4979137fc/ply-3.11-py2.py3-none-any.whl", hash = "sha256:096f9b8350b65ebd2fd1346b12452efe5b9607f7482813ffca50c22722a807ce", size = 49567, upload-time = "2018-02-15T19:01:27.172Z" }, ] [[package]] -name = "apache-ossie-ontology" -version = "0.1.0" -source = { editable = "." } +name = "prompt-toolkit" +version = "3.0.53" +source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pydantic" }, - { name = "pyyaml" }, -] - -[package.dev-dependencies] -dev = [ - { name = "pytest" }, - { name = "pytest-snapshot" }, -] - -[package.metadata] -requires-dist = [ - { name = "pydantic" }, - { name = "pyyaml" }, + { name = "wcwidth" }, ] - -[package.metadata.requires-dev] -dev = [ - { name = "pytest", specifier = "==9.0.3" }, - { name = "pytest-snapshot" }, +sdist = { url = "https://files.pythonhosted.org/packages/7d/ea/39b988c938f75cb75d7045b5c69f8bfed47ee2152c8837fb403de29d6fb8/prompt_toolkit-3.0.53.tar.gz", hash = "sha256:9ec8a0ad96d5c56148b3f914aa79c1564c3fde5d2e6b876e7bc327e353cf8fa6", size = 435492, upload-time = "2026-07-26T20:56:14.758Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/6f/84908cad2d6aa5144abcf7b42709fe4fdb459bc640ec7ac5786e7693dabc/prompt_toolkit-3.0.53-py3-none-any.whl", hash = "sha256:01c0891d7f9237d5e339f7d3e42cdae80b7534abb1c7c0e3352efba6231492f2", size = 392288, upload-time = "2026-07-26T20:56:12.512Z" }, ] [[package]] -name = "colorama" -version = "0.4.6" +name = "propcache" +version = "0.5.4" 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" } +sdist = { url = "https://files.pythonhosted.org/packages/b3/9a/9fbf4e4ec0c2d7f1c32519fff782ef467859b8faa9fbc5331a96f6395d43/propcache-0.5.4.tar.gz", hash = "sha256:ff6b113f50bc066a698db5d944d2c6dc7507168dd3341e255a8892fd0715a558", size = 61545, upload-time = "2026-09-16T00:17:14.386Z" } 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" }, + { url = "https://files.pythonhosted.org/packages/1e/40/14b21e505b7921617466576423f188a5c9caddfdaa1cf4b2b8a83d8fe216/propcache-0.5.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:897d1ddf6716e8f47200f7aad9a0efa6cc7586df66c6defa572f9eab379c078e", size = 86393, upload-time = "2026-09-16T00:14:06.9Z" }, + { url = "https://files.pythonhosted.org/packages/e7/4b/5a52e1a7b43563f7d408814194bb23cc8bf214eb6b86639b667a33a8d0d0/propcache-0.5.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9cbfff4423eef4cc6cafc021469641a2b835f610b2647a6c5281903e21b8670d", size = 50431, upload-time = "2026-09-16T00:14:08.025Z" }, + { url = "https://files.pythonhosted.org/packages/05/cf/b5248180bf056cc76acc60c9c6e8c0ebbfdbd1c6cffd31fd14996927b7c8/propcache-0.5.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3fc24f209c1b7f7f688b66b98293954f5504279760999b58920ee12dd8471c1d", size = 52116, upload-time = "2026-09-16T00:14:09.114Z" }, + { url = "https://files.pythonhosted.org/packages/86/a8/7c6cd6bfead1a11f2e411e688640e6d26574cb0bde7dcaa7423b0b65ed7a/propcache-0.5.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:62530ca89187827e4a4fe733f971abe81a7542eeea48ff61995f19b64d7199c8", size = 238729, upload-time = "2026-09-16T00:14:10.357Z" }, + { url = "https://files.pythonhosted.org/packages/5c/b4/442715b2e980df51be52d203549279e027728f24c80b00b5e525e31cd5ea/propcache-0.5.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:56fc3f7599528db40b1efa0889a620116e2704144495273d66066e8164e45838", size = 246121, upload-time = "2026-09-16T00:14:11.735Z" }, + { url = "https://files.pythonhosted.org/packages/bc/5d/df0684fc2b1732a01a7bec26d7897369022712422d25b09c37ce7dbc88a2/propcache-0.5.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4f2d880ff60f45898f4acfa152aac8d04e3ee627d90ff4003491bf92239d5757", size = 251735, upload-time = "2026-09-16T00:14:13.053Z" }, + { url = "https://files.pythonhosted.org/packages/c7/06/519a5ebb48b6f94beb48396e55c905f12246a25c3a3608a7ec7bceabf50e/propcache-0.5.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6e9368e87a3efc285e559131092c5db643eb8e56de4ee42064d5baec22ef2bb5", size = 235381, upload-time = "2026-09-16T00:14:14.398Z" }, + { url = "https://files.pythonhosted.org/packages/3c/07/1e0a9bb310830f2245edbd5cd3c6d24a783c053c4efd8e08e386e513c940/propcache-0.5.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:004e685b315646c410771836e72a44f143bbe624f29653a42687815069a303d5", size = 208973, upload-time = "2026-09-16T00:14:15.715Z" }, + { url = "https://files.pythonhosted.org/packages/62/5c/9324fab27d6088eecc47fe4332bf7aaf8c1ded93c36f558391e8a06d41a7/propcache-0.5.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:594eb4c6ec35e7179b058481f4e9f02521b56de16fa577c4b85c76fb1bf8a9f8", size = 233897, upload-time = "2026-09-16T00:14:17.25Z" }, + { url = "https://files.pythonhosted.org/packages/9c/a3/570d92fc952eae93b676f3a1568f4b89264102abd3c982ab6a9ebec58dcf/propcache-0.5.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:2dba2f02d2d5c09ef8a0e6c1a42aeaa451f4be9898cb00b04fe98717da2eb23b", size = 223512, upload-time = "2026-09-16T00:14:18.87Z" }, + { url = "https://files.pythonhosted.org/packages/65/47/26810d889d89bba31db397e6a88f8984af775f5ed6bad0a29dce84324cff/propcache-0.5.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:c3ef2818d63bc86071e9d2989ae75a1bc32b8f7059cfd9f5abbbee70c32e2ed6", size = 239043, upload-time = "2026-09-16T00:14:20.366Z" }, + { url = "https://files.pythonhosted.org/packages/89/2d/f9c47691aa024c8299a3afacd78d22a01ab57eb627b481b6089708e71017/propcache-0.5.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:dd2ac8f5b643454c2cc6b6118b13da16e88f4a6434fc3ba61aca384029f04f36", size = 208218, upload-time = "2026-09-16T00:14:21.801Z" }, + { url = "https://files.pythonhosted.org/packages/74/6b/d510c0c378cabbf9d0ac7b663af6d00f2e9074073d93b20c85f24aa5c071/propcache-0.5.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:4054acf80d40456a0537f2913b349718649d8d6458a14ab7f48d0ce28c30869d", size = 240301, upload-time = "2026-09-16T00:14:23.121Z" }, + { url = "https://files.pythonhosted.org/packages/3d/80/c80f6adaaa1e51f0db2dce8c9b3714d94ec45e358a21f9a1910b10b40a80/propcache-0.5.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:40e94adb1e7d39ff28a8bd8d8b8fbd1df6b9f40976dbe379134f1ce058e532dd", size = 230785, upload-time = "2026-09-16T00:14:24.458Z" }, + { url = "https://files.pythonhosted.org/packages/d9/e2/c32a7df3f39caa7f11b2eb37ea5b6960a6946f2bc7c4b8ff97bbdf6d6b6e/propcache-0.5.4-cp311-cp311-win32.whl", hash = "sha256:9f86f7259efe2c951f43e57d471c9b41daa5bfc7db9f67189059cf1ae6d77fd9", size = 42747, upload-time = "2026-09-16T00:14:25.715Z" }, + { url = "https://files.pythonhosted.org/packages/0a/8a/3db6a3543d8101263b4c52978b6276a04ead2caff2c5ab880d934f47bd89/propcache-0.5.4-cp311-cp311-win_amd64.whl", hash = "sha256:e904d4d01f36bd6e197590be1533c44e06058771e0746dd073a8ebb3ef880858", size = 46268, upload-time = "2026-09-16T00:14:26.996Z" }, + { url = "https://files.pythonhosted.org/packages/41/07/5222e2665bbf6e45847492ecbf3b9f3e4975a0ae300e5fd465df7d48ce55/propcache-0.5.4-cp311-cp311-win_arm64.whl", hash = "sha256:d42a9a856a4a6e2f6c10f1318c07e7daa498d6593abe745c71dae4521a26ca39", size = 43547, upload-time = "2026-09-16T00:14:28.143Z" }, + { url = "https://files.pythonhosted.org/packages/71/cd/348d58f142aebc4873345c6b31087629182ca6e0f2b3caeaa528cf882eba/propcache-0.5.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:b28f41fa3b8c6900457f858ec5b03998f3a6d535fbc1bb2edec5961ea05ec429", size = 87285, upload-time = "2026-09-16T00:14:29.362Z" }, + { url = "https://files.pythonhosted.org/packages/df/f4/f3ffaee281b276da854ac1d7a6a506d26cbc62ea2e623756f1d0a4a1ba1a/propcache-0.5.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:dcbf346a318a5e30063f547630b02bb787ce2f45b6368d5da143660b6a3835d8", size = 50984, upload-time = "2026-09-16T00:14:30.473Z" }, + { url = "https://files.pythonhosted.org/packages/25/88/1d7df7201750b37765ef2b23bc1c526c028dadde80afa0f57a118fc01182/propcache-0.5.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:87a3caecf8095e48dc72f84bfa42e23a848cf410cc9cc13031fba4869b706a21", size = 52460, upload-time = "2026-09-16T00:14:31.692Z" }, + { url = "https://files.pythonhosted.org/packages/83/4f/48865bd02a16ee5236bc46166b2946f37b93e07b0eae355dac0be0b216ca/propcache-0.5.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:60a64cbccaa11b7760ce705a14ada17ba459e7ca9f23ba587eb013821032d7ef", size = 251768, upload-time = "2026-09-16T00:14:32.908Z" }, + { url = "https://files.pythonhosted.org/packages/b0/19/3742a5eed62317b03b4002ee865dc9fd720308bdd0da1f29a5786c630311/propcache-0.5.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a74bfa37147cc08fb29df10bd9c16f40fa7f860cd3a6d2fff853323a94f6e17f", size = 257723, upload-time = "2026-09-16T00:14:34.267Z" }, + { url = "https://files.pythonhosted.org/packages/cb/d5/ee6350fb0be9122bb6c67082a876d34b90d980d100c106af4b81023e04f4/propcache-0.5.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a4d7a54719b67338a305dca2ce6aafe366817df94ddfd4b5514374356f5ca546", size = 265597, upload-time = "2026-09-16T00:14:35.56Z" }, + { url = "https://files.pythonhosted.org/packages/85/9f/83a07b6ec0e043c050cfdd35fb0cf1b7897b91d554d6eea293740309afe7/propcache-0.5.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2814ecd8e818f487bee4b0f921bc4d1c176cc5fc71ac0f072d0fa67eda4ac14b", size = 250424, upload-time = "2026-09-16T00:14:36.894Z" }, + { url = "https://files.pythonhosted.org/packages/33/2c/a763a8251f50fba042af0fb1f02bfec4b31381e40aff760db2be7b2e1f84/propcache-0.5.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6af4693716bfb03f1752ef1b30faa593db2c01d5272e9b8564a1549452a979ab", size = 216748, upload-time = "2026-09-16T00:14:38.369Z" }, + { url = "https://files.pythonhosted.org/packages/6a/e2/4d11bea8fd6a777149c6c20645f873952eab5de3a2497aa11648ec9ab6ab/propcache-0.5.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4fbc1a15dc8cd1689508758d626b372b1f09d28d9577667feaf9e6bfcd8efcbc", size = 246533, upload-time = "2026-09-16T00:14:39.82Z" }, + { url = "https://files.pythonhosted.org/packages/9f/36/6683597de4907e70c717e3588c541202c66086a72ff3db58be49de66e72c/propcache-0.5.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:cdee8205a44d0be91bbac4c41b95d86641b72dfc7aef1279400e4fda3f26a937", size = 238173, upload-time = "2026-09-16T00:14:41.259Z" }, + { url = "https://files.pythonhosted.org/packages/85/84/cb08d79f1762daafeb2b030c470cd0c725c97b8ad67412457c6f35c53e9d/propcache-0.5.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:9a2a8a50a93dee0268a860a07fa3b4bd968f8ce4dbd794957da772f395368526", size = 251128, upload-time = "2026-09-16T00:14:42.652Z" }, + { url = "https://files.pythonhosted.org/packages/c2/0d/41b848036db6621370c1f2e5471a7da8149c730f8552a5257567721f4576/propcache-0.5.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:7ffafcbfc7b549ab940047e505c831eabac5e67de53e1bc174adbc5285c55944", size = 214821, upload-time = "2026-09-16T00:14:44.112Z" }, + { url = "https://files.pythonhosted.org/packages/f1/b7/adfae4bf9c63bccf12e2d9690a175c6579047a6eec3b5a6a5f51428c15e2/propcache-0.5.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:d1f5a500bfcbb2c0ab85e98a0dcd70f5899d34efe365a0187700369a79603031", size = 254793, upload-time = "2026-09-16T00:14:45.429Z" }, + { url = "https://files.pythonhosted.org/packages/51/6f/eeca9647245d5f92e87d53e5f14335bb42fce1a7e6842c8045b364eded8b/propcache-0.5.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8a235f73d6e020855dc29dff012d920c02ee0feab8d73a24185a7569f4be1161", size = 247134, upload-time = "2026-09-16T00:14:46.976Z" }, + { url = "https://files.pythonhosted.org/packages/5d/a9/424e38838793d37160b4379c702f61c74c598fc6cd17204adbe3c554f7a8/propcache-0.5.4-cp312-cp312-win32.whl", hash = "sha256:b3083bfe87f95c756e610bd8025f26cbd1cd4aaa03a422f2d65efb7a97cd53d8", size = 43073, upload-time = "2026-09-16T00:14:48.338Z" }, + { url = "https://files.pythonhosted.org/packages/58/7b/6e8ef26f6d510a7916064fec68d55fcbfbdf7eb01e377480d66a122152d8/propcache-0.5.4-cp312-cp312-win_amd64.whl", hash = "sha256:98914de2c4d7f0f9f4a8c6ea4bf05841f4175796941e3ef7d47eb718f22311fb", size = 46190, upload-time = "2026-09-16T00:14:49.99Z" }, + { url = "https://files.pythonhosted.org/packages/08/b9/72028c5b56ced97f456de6aefa79435ca64d7f77af78ea8cf3c76fc5195f/propcache-0.5.4-cp312-cp312-win_arm64.whl", hash = "sha256:8876b39961e33d912afe3c1bee18ee564fdad0206f873cc15d522756b7f50737", size = 43075, upload-time = "2026-09-16T00:14:51.155Z" }, + { url = "https://files.pythonhosted.org/packages/78/4c/3b1365d58a667689e067e13d055fcd92bdf8d9a2fca3d9201b47ed5b3631/propcache-0.5.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:36c0d9db44b523ef93d03341b1c42d69ff01d673c053d1b1c6c3a363bcaa39ba", size = 85290, upload-time = "2026-09-16T00:14:52.342Z" }, + { url = "https://files.pythonhosted.org/packages/8f/61/5f9c29c3aa67c30238c4eadf95149b1d983a48f69b86b0cff927a7d6df13/propcache-0.5.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1d52a05dc417279f7e5c7618c5dfbbc29923aaf9bc0a5c1802ddcebf54c61a0", size = 50027, upload-time = "2026-09-16T00:14:53.67Z" }, + { url = "https://files.pythonhosted.org/packages/25/7d/c1ab1ef09e9d4d835be5d58c0a32a1e1de8397abaa4e502a9d4141328cad/propcache-0.5.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:44149f46500a0a41b95b4d99c2e586a77319539730607b9892974a092788b111", size = 51425, upload-time = "2026-09-16T00:14:54.826Z" }, + { url = "https://files.pythonhosted.org/packages/73/36/0093091ebb270fcd1bc1f6e095f93b2e0ed7f1011c28837dc2dbe5f96b99/propcache-0.5.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dbab5f5ff6897c81f355d079010cdae85b02e5a0b518b5251523b8ad8ae9ac3c", size = 233595, upload-time = "2026-09-16T00:14:56.09Z" }, + { url = "https://files.pythonhosted.org/packages/ae/8f/0de9d4c8e05ce0be71b436919a216bd7fc5cc6e2691c0602295efb22b9ed/propcache-0.5.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c3e98c55bde2bcf7db3c70d1aed7ae9aa8aebbf19a250c66645cde44cdb8b867", size = 240318, upload-time = "2026-09-16T00:14:57.674Z" }, + { url = "https://files.pythonhosted.org/packages/7d/71/2b35e91455209b85ee98f7859583e0814fab57d3af0f2381aaee34c37304/propcache-0.5.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:db3ae52ccc150dbc84704e9d642743897f3e1c54742ff34cacb661e52e3818a9", size = 246649, upload-time = "2026-09-16T00:14:59.352Z" }, + { url = "https://files.pythonhosted.org/packages/ed/74/08e6c1faf26ee2732023a3828787ba535557122774f4a386b1f715cbd8e0/propcache-0.5.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f85915e00dcb1cd9f2f890ead064ed40a27df06f0db65be427b29482ae357572", size = 234316, upload-time = "2026-09-16T00:15:00.696Z" }, + { url = "https://files.pythonhosted.org/packages/5c/9a/08385733c9321c9bb78039d3ff31045e4fca962d9665023c4eb70f998819/propcache-0.5.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c2ba30a89035b57b73e00475de948521602f543d79ce01db10b04b36c4c76fc8", size = 204666, upload-time = "2026-09-16T00:15:02.019Z" }, + { url = "https://files.pythonhosted.org/packages/1d/f4/e87bc7629af9a14a752b218764a78742d73c2c563ac58315da6841f0cbe4/propcache-0.5.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ae58f361bd5dae942717c65d3413b478c70aea9c462599e7b9adad3731db3894", size = 225900, upload-time = "2026-09-16T00:15:03.394Z" }, + { url = "https://files.pythonhosted.org/packages/d9/6d/11014938d3fe9bea2ea2dcf930f26ed565bfb2f5be3c756362ea48c92636/propcache-0.5.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:96f7c5c15656040ddcbc51e56dc59b58aa25999d743c126abd425b9766ab43e9", size = 219988, upload-time = "2026-09-16T00:15:04.811Z" }, + { url = "https://files.pythonhosted.org/packages/dc/72/fbf17c589f92c0b3bbf6709a425661f8ef2ed0d46b38985a7d7b5a0f6b91/propcache-0.5.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7cc528e760a8af06f2b13e9b9f362cd90c7c718ea61228a96dbd31ba16ed7f47", size = 233611, upload-time = "2026-09-16T00:15:06.498Z" }, + { url = "https://files.pythonhosted.org/packages/55/7e/dbd637572a279692e5518d117274a9331bf5faac59f191d30e82521a3ec7/propcache-0.5.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:425f8cc86ab5018b4b8d4a23bc8e74d964bd3d757c3702e301aa79be76c53f6c", size = 204333, upload-time = "2026-09-16T00:15:07.961Z" }, + { url = "https://files.pythonhosted.org/packages/ba/5a/f99c92068f1e0f5c886899ce0e4a619db376ca98c5279d93f95bd86906af/propcache-0.5.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:a5793c7698a53f56f4a1889a4737c7eeb1b7ad0842fa6b1abca22913ff79c8c1", size = 235177, upload-time = "2026-09-16T00:15:09.334Z" }, + { url = "https://files.pythonhosted.org/packages/ee/28/95456fabd2daf6be89049a13fbf03341756014d2959c83d12957d4c49694/propcache-0.5.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c02c0e570c5c7e077b0181a9f3cdb7d4c3617d1cda6b5c95bd5d34022923d82c", size = 228982, upload-time = "2026-09-16T00:15:10.729Z" }, + { url = "https://files.pythonhosted.org/packages/b1/bb/df90f62c9cf7c93ea235f6f9405143bba802914607317266dd81fc8d737e/propcache-0.5.4-cp313-cp313-win32.whl", hash = "sha256:3e413d7a4a9b4866b7a761d6060d434b64d23cd35122eda3b026a0bbe8196b25", size = 42611, upload-time = "2026-09-16T00:15:12.111Z" }, + { url = "https://files.pythonhosted.org/packages/01/bc/e0a7b84af04ec02d73a48aa71f091e1e4a2107e3074b7ce12195b66901f4/propcache-0.5.4-cp313-cp313-win_amd64.whl", hash = "sha256:0c889f6fa84957bc7e8b4eab71fd16a0455068d5045e3aa40c733071d2b2fd77", size = 45342, upload-time = "2026-09-16T00:15:13.519Z" }, + { url = "https://files.pythonhosted.org/packages/9a/70/50b031cafe72a5c1878b903ee87303f71313345566bf3d6ec202e5ddc9ec/propcache-0.5.4-cp313-cp313-win_arm64.whl", hash = "sha256:69fc35c0779522da366c563e5faf203ffc1f8ff0021d5b1337fa4efa5be73177", size = 42408, upload-time = "2026-09-16T00:15:14.788Z" }, + { url = "https://files.pythonhosted.org/packages/33/c9/07e227b930c8ae513b8ef1aae3793499be097bffcdf7aee4fb8b33db4cd1/propcache-0.5.4-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:e6720ba44ad7e72174314d0e1fb0172494cff5c73a3a8a2159c3d2402ff15565", size = 85933, upload-time = "2026-09-16T00:15:16.073Z" }, + { url = "https://files.pythonhosted.org/packages/e4/e1/6710bb44510c4e4a8e0f004bbaf3cecfd048141309c77bae56d4e5a6ebc1/propcache-0.5.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:4cfe0a92ae30151869e67a4b5f5e105e4e03ad30b3f38e5211b5bf77d0881993", size = 50179, upload-time = "2026-09-16T00:15:17.377Z" }, + { url = "https://files.pythonhosted.org/packages/e2/22/b533b493d7025456f44518b33e53e000021a20fe7c27b88cf3d341df7186/propcache-0.5.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d759d05634f1b038fb625a66662a8c85e5a8fec912da381b5149ddac107482b", size = 51942, upload-time = "2026-09-16T00:15:18.589Z" }, + { url = "https://files.pythonhosted.org/packages/f1/74/70ac8430e28f21e442c7bcb964eb46c4363f6881ade4aa0e978bfd8d503a/propcache-0.5.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:251c63dd46a0659bb875cb254dc4c1e79ee91a847c737cd62373295afc2235dc", size = 232647, upload-time = "2026-09-16T00:15:19.905Z" }, + { url = "https://files.pythonhosted.org/packages/72/95/f222f13b6fe623310be0eb61a673bf26df439ce27e563ca8e422d0818777/propcache-0.5.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7a8d5ff04eb1f85698a78d20c62a14676e7b960dcafde09a388d60ad377d355d", size = 241541, upload-time = "2026-09-16T00:15:21.3Z" }, + { url = "https://files.pythonhosted.org/packages/a2/3e/763e370340db16115c5e63ad46e21ef0770a7f06928b3d3b62d8f8edfca4/propcache-0.5.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7b9100a93b372418d8688f3f2a3e5b45c64d70ca4d6176e121aca1e3bfc1e32f", size = 245332, upload-time = "2026-09-16T00:15:22.802Z" }, + { url = "https://files.pythonhosted.org/packages/96/d3/e97cd6f5de2176bd90ed4076c7a9b5e09d0f0b9687d00a576507988bb62c/propcache-0.5.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cc07876cfb079b6f6f36d21ce75784ad6c2c6b563eeac0ed26c2fa2669b85df9", size = 232757, upload-time = "2026-09-16T00:15:24.374Z" }, + { url = "https://files.pythonhosted.org/packages/f9/4c/6766e5f60bcda26d244333aa71d0a702c1c9b21b251d543c7af5953d1eee/propcache-0.5.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0951315a6b3142ee2167404d707743f0157c110091342b1aa0accac5cf0e4acf", size = 204389, upload-time = "2026-09-16T00:15:25.667Z" }, + { url = "https://files.pythonhosted.org/packages/b8/5e/ec4bb09a70b26ea99d76a8292c3383b960b296de2b347ac9986678f1761c/propcache-0.5.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:bee7d3aed13d56f54e681df38c3a23031bc9e3863f687d9d598825c9146acd7d", size = 228217, upload-time = "2026-09-16T00:15:27.11Z" }, + { url = "https://files.pythonhosted.org/packages/e1/7d/b53922ba7d9e5bf797324e63aa05906ec240871899f779628df068743e2d/propcache-0.5.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:4e985382be6d15da8d0c2710a6fa7b9070fc9ecdeefb7f580e88373984ec8be3", size = 216947, upload-time = "2026-09-16T00:15:28.532Z" }, + { url = "https://files.pythonhosted.org/packages/ff/39/b62eee45e5ea4de094a258cbb3b01c1e856ca51ddfd95b43135c5effd1eb/propcache-0.5.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:9e9ab13760aa8b6d0881ae7cb04fd891d8d490cd2554ea8e79bb278399169bcc", size = 233457, upload-time = "2026-09-16T00:15:29.977Z" }, + { url = "https://files.pythonhosted.org/packages/cc/a9/feec61ed296d993db9dd097e0f6723e3f576a647722367547495e4c5b05c/propcache-0.5.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:1b2f3bec4261a94019575481c726c29850f72e27907773c75b1de421e20e9f9d", size = 204131, upload-time = "2026-09-16T00:15:31.74Z" }, + { url = "https://files.pythonhosted.org/packages/92/4d/411ef380cddad28dc001f1c6d75ec72c76cd3817030f68ec1ccfba0ec6c1/propcache-0.5.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:720cf832eb2d0b0dfee129cb3335a26f6ce3cc45ee1187e8f0731758caa16792", size = 234820, upload-time = "2026-09-16T00:15:33.087Z" }, + { url = "https://files.pythonhosted.org/packages/15/37/c988229753629ef1cfd5198337a83e624780ea2b3787efe9e747c05aad2d/propcache-0.5.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9fb0a5be8d9aa213150e8d8148a42aca4984b285bcad1e69587dc4298edd929b", size = 228350, upload-time = "2026-09-16T00:15:34.533Z" }, + { url = "https://files.pythonhosted.org/packages/12/49/5ef1c5cf98591da3c5b952b39e6a298084cc1ce353bc70f85e82397a5036/propcache-0.5.4-cp314-cp314-win32.whl", hash = "sha256:30cc1cebaf9aef49db06357a50398323ae04d70460c0491837d026ab7d6452ea", size = 43578, upload-time = "2026-09-16T00:15:35.957Z" }, + { url = "https://files.pythonhosted.org/packages/1e/9e/a0ac821a2229186af5e2e3c3635a78abb23cfddca57f38513ab5d70420f3/propcache-0.5.4-cp314-cp314-win_amd64.whl", hash = "sha256:0a095db8e15a6020db149ecbed6461939fe74f6acaa3ae8b702a1fe8c38cd983", size = 46304, upload-time = "2026-09-16T00:15:37.655Z" }, + { url = "https://files.pythonhosted.org/packages/a1/19/c8d0d36a9d16cba5dcee67d389c9333b988c8986a653a61c00a451817a46/propcache-0.5.4-cp314-cp314-win_arm64.whl", hash = "sha256:45488d1a5f9ab5bd90aaa1ca20f50fe1922b8ffad71a2009d2adf41355897aac", size = 43440, upload-time = "2026-09-16T00:15:39.091Z" }, + { url = "https://files.pythonhosted.org/packages/2c/e9/42f1da77cacfc184e6ec929557ef653b7961bbf6f1da460b9221273948b3/propcache-0.5.4-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:53eaa697c4d0422ff4cb714d00231b43352064d97b944033b30c1d57cc506ec0", size = 90672, upload-time = "2026-09-16T00:15:40.306Z" }, + { url = "https://files.pythonhosted.org/packages/cf/2f/4b79940908c6ab8c795097c102999d7bc1f7e0b8604dfd1c232f9d99d67a/propcache-0.5.4-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:886b59c4d28ca97dd23b025fdfc50a0356be934efbbbca89ad26230067f86fe5", size = 52586, upload-time = "2026-09-16T00:15:41.575Z" }, + { url = "https://files.pythonhosted.org/packages/eb/07/02196ae6320c110235bb343f90dbd34be41f8b8964a3ee30db84ec12579e/propcache-0.5.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3fa15757fea1dfcd5b7745cad9f4638929605531bd4018ab2adff7955f1a403d", size = 54335, upload-time = "2026-09-16T00:15:43.027Z" }, + { url = "https://files.pythonhosted.org/packages/6f/44/f48b9a131985659924df5fa5093f68fe72c7ee375329802989ba3126efc6/propcache-0.5.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6f0093ac3e9daada202c2082439d414a625c57184727a46e112a3fb2a81cb788", size = 297567, upload-time = "2026-09-16T00:15:44.373Z" }, + { url = "https://files.pythonhosted.org/packages/04/a1/418d956d2735139f77fc35262179f1f52c23aa666de5a8ab3819c1ae7854/propcache-0.5.4-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3cd3a7edb6b95b9b33998135ebfa18d709da82290fb8f27c858970b5a12c8b56", size = 297477, upload-time = "2026-09-16T00:15:46.048Z" }, + { url = "https://files.pythonhosted.org/packages/69/fd/ff811fdb6d3d3e67fd9bbfb75881675d34a42d0ef29a45d33e3e233dde07/propcache-0.5.4-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c174bfd1c48a1b51a3078e95586dde718374bac79719ab3541ec9e74aec40574", size = 302669, upload-time = "2026-09-16T00:15:47.458Z" }, + { url = "https://files.pythonhosted.org/packages/fc/57/527910c455b5ec62f6871bef45d4f79fea16cb8c966ba0d4a07f0339ddc4/propcache-0.5.4-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a219f0ac59817a9114dd2aa57c13180f993e819ba658c7ddab4b66ed1ee0d370", size = 287908, upload-time = "2026-09-16T00:15:48.99Z" }, + { url = "https://files.pythonhosted.org/packages/1d/86/f69ab82707534a0cb2057bdca04f9200a71214c7551800f9d34d6ac39e4f/propcache-0.5.4-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:17a7400cec0256f0a71ae71f9da398f9894c956ff6668a1c9d317b3367316320", size = 249804, upload-time = "2026-09-16T00:15:50.486Z" }, + { url = "https://files.pythonhosted.org/packages/27/19/60677af50d93be4256213de7cd487f056944c048b9c0b6f2e45b3a30f666/propcache-0.5.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:978f28401afbc76cdc3df9e1717b4229a06b626a1dcc75db4e1f2beb3884c3e9", size = 282344, upload-time = "2026-09-16T00:15:52.029Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f7/a0057808a91fb3b6a5f3602b528f0cdcb3d53e0ff8315d73fabdfdf8fec4/propcache-0.5.4-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:4a1f4f5ffa55dce6307631f3cb2948e117e665966ea512e0d502b16c24f567e7", size = 270167, upload-time = "2026-09-16T00:15:53.466Z" }, + { url = "https://files.pythonhosted.org/packages/83/c8/f4a865490df0dc0c8531d4e59ac411cb6dc24bb255d2396a6f1c60a368f4/propcache-0.5.4-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:213bb68d9ced5cf2bf717b1071bf2b09b4b04c426256f9fe6d054c60318424c4", size = 286551, upload-time = "2026-09-16T00:15:54.995Z" }, + { url = "https://files.pythonhosted.org/packages/b0/67/b4faebde9da4e8173d0e5a30e8cd31335914af7ef350b988f27fec588cfd/propcache-0.5.4-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:286867fb156488c251a3721766e380ac4495e4fd6b51aaa1403d89ce7f4359d9", size = 249595, upload-time = "2026-09-16T00:15:56.505Z" }, + { url = "https://files.pythonhosted.org/packages/f6/40/52e1dd5636e9f5a27f6b5a4b4e2f33c322fd72afe956c397d82523ec4a80/propcache-0.5.4-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:445ee3bfb46e85838387fb3c536a73cc0b994dc192b004e40e170adc54aa2a7e", size = 286700, upload-time = "2026-09-16T00:15:57.985Z" }, + { url = "https://files.pythonhosted.org/packages/d5/0e/30b2b324b93ff31a0bab539c102aae59e84e444031b2742150a7646aa1bb/propcache-0.5.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:48cb48c5346a97de792254af77715aa2529c2a1ebc5f586aa0aae44a02f1fe57", size = 280500, upload-time = "2026-09-16T00:15:59.487Z" }, + { url = "https://files.pythonhosted.org/packages/64/36/721bb59f682ff060d0c8df64274fca8cd0521b1a54506c2eedaef795b7f5/propcache-0.5.4-cp314-cp314t-win32.whl", hash = "sha256:03b229037d25b801e7af53fd52b9fc49d9439b036fca1e087e02780631adfa97", size = 46121, upload-time = "2026-09-16T00:16:01.349Z" }, + { url = "https://files.pythonhosted.org/packages/c1/86/0b1b80fa1ac3a0aac44e2922a6964fbe9cd52af5eab8fa933bf9e90b030c/propcache-0.5.4-cp314-cp314t-win_amd64.whl", hash = "sha256:8a1fc236528c457cd739c88abe823da851b7ab645d72792f88658114cc340c12", size = 49154, upload-time = "2026-09-16T00:16:02.901Z" }, + { url = "https://files.pythonhosted.org/packages/69/4f/9fe6f05a47cb550c823155052116f710064b6be5c6e8ec4e9faae7e18115/propcache-0.5.4-cp314-cp314t-win_arm64.whl", hash = "sha256:135036c5cfc93864affb0f9af9a27e5d7a71cb7bd745e7b6dbfc2d56cc30e827", size = 46005, upload-time = "2026-09-16T00:16:04.266Z" }, + { url = "https://files.pythonhosted.org/packages/58/25/895a11d1e4c5c2acc6d816e2bece34e02d9dc92f2182ae276cd819e9e804/propcache-0.5.4-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:45bf2e730ab8905d0527fe05a86500f406e64305c34cc81ebe64b4617cab9760", size = 85634, upload-time = "2026-09-16T00:16:05.599Z" }, + { url = "https://files.pythonhosted.org/packages/58/41/c0acd69271de7a1cf439e77d5d60c18575fd09bad56e798b95fa23458ea4/propcache-0.5.4-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:31eb43ba2edc704ab2ec27815315dd8a19def0fb16215be4cfe8d32fe78ffd51", size = 50084, upload-time = "2026-09-16T00:16:07.384Z" }, + { url = "https://files.pythonhosted.org/packages/a5/1a/ad561f99f90884089e6403b76c220610809429ba868a81a2e7ce115d32e0/propcache-0.5.4-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:174507f82d3594622acb1dd2dafecf2d899d6d506335494e7107767bf05f3aae", size = 51692, upload-time = "2026-09-16T00:16:08.956Z" }, + { url = "https://files.pythonhosted.org/packages/e9/07/057bdd3a9609ffad59b06239cceee784b047f6c720247bfaa36d2103e138/propcache-0.5.4-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:50e337653721d20ead710da33bf44487fbe8a0db8782714b60306481e9f95b51", size = 232947, upload-time = "2026-09-16T00:16:10.466Z" }, + { url = "https://files.pythonhosted.org/packages/fa/dd/d36ad35986718530498a65e45e3713f9f0e6a580f192ef02d2ef7cae9b52/propcache-0.5.4-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0d21d0d2c82bbfeb1677a9711f38df968f9837576102bb4add1bd449d28d88f1", size = 241250, upload-time = "2026-09-16T00:16:12.056Z" }, + { url = "https://files.pythonhosted.org/packages/fb/81/f1459415cdb6c10d46942779de39bb59a77b38e5a76bb1def9227962eb45/propcache-0.5.4-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ccf4f7a79e26bb7efb06ecd50c177833b71df05cbc748701372325e6bcc17f6f", size = 245150, upload-time = "2026-09-16T00:16:13.596Z" }, + { url = "https://files.pythonhosted.org/packages/ce/4e/58b9b1460afc97a4c0b17ee89af701c4011d4d7f46470eba3aaff76a8069/propcache-0.5.4-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:23278f808cd81d5ada7184a76606b925fb3389c60e1077b2cd7da7b1fcf0553c", size = 232166, upload-time = "2026-09-16T00:16:15.126Z" }, + { url = "https://files.pythonhosted.org/packages/b9/c6/5a79e0eda3e7b6987d03d8c622ff6d52a42165a12e8418eb37694b9cc4b4/propcache-0.5.4-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e738ab81179510ce79b2eac9a6ecf47feffd9e76d1c72e403005dddb6e36c06c", size = 206085, upload-time = "2026-09-16T00:16:16.713Z" }, + { url = "https://files.pythonhosted.org/packages/4e/72/940aed42c73f9da345ca2de0f6e835c726498159abca5f1ef14fb0a2af8a/propcache-0.5.4-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:a419ee85e654927baabda3929c03c0cc1112bf472ff0dfd6142f4e3a81ca4162", size = 228460, upload-time = "2026-09-16T00:16:18.352Z" }, + { url = "https://files.pythonhosted.org/packages/85/71/3f54e1535c8f323d91ba566044d7c2b39ff6f6a2f1d0bd9071779d07b9b3/propcache-0.5.4-cp315-cp315-musllinux_1_2_armv7l.whl", hash = "sha256:b61805357d966680acf68b3b6d49772631ed9df44ebece10ff1460e117a7da8a", size = 218350, upload-time = "2026-09-16T00:16:20.064Z" }, + { url = "https://files.pythonhosted.org/packages/a8/f4/025890cc389ac3ec485ecec607d4a7ca47e15bfa2a465746ab98af602536/propcache-0.5.4-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:58134228927cee6c047d626c08e60a81be604a20578a12ce752cc5c9a84d4826", size = 233156, upload-time = "2026-09-16T00:16:21.624Z" }, + { url = "https://files.pythonhosted.org/packages/04/29/b39cae08c87c140d3d274f0a2c058cb5588e836175c3309e260b230ab07d/propcache-0.5.4-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:350b272b2279f4135a64fc0c304a5d08e28a137c9573442c606152446638a831", size = 206206, upload-time = "2026-09-16T00:16:23.204Z" }, + { url = "https://files.pythonhosted.org/packages/18/61/e16462ef18a87247dc9ebbd5c606f46d5ce67e708bd9cc734dd0d9222564/propcache-0.5.4-cp315-cp315-musllinux_1_2_s390x.whl", hash = "sha256:45bebbe252550fec975ba3b62bc6f931643cfd3b5464ef47619cf3fef154e01c", size = 234469, upload-time = "2026-09-16T00:16:24.841Z" }, + { url = "https://files.pythonhosted.org/packages/9f/84/b6a1490922427204fc47df920ed002eec709621de6b79b11592bf45c623a/propcache-0.5.4-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:ada748108a43d29b7c328ba7db3755327cd94f028bcc1a7ee3f0addcfacd9c38", size = 227311, upload-time = "2026-09-16T00:16:26.549Z" }, + { url = "https://files.pythonhosted.org/packages/ff/5c/5a59527582e9bcb694b2f08b9894134b65a0f5f79dbff174f054f5f74ed0/propcache-0.5.4-cp315-cp315-win32.whl", hash = "sha256:ee19113bce2f3acd46432050688b70f61acd6857d75abb9ec96341b7e9ced123", size = 43512, upload-time = "2026-09-16T00:16:28.313Z" }, + { url = "https://files.pythonhosted.org/packages/26/07/93cf699ed363681e754d7c3fad587fb09ef6b65618ee193332ad16a68d7b/propcache-0.5.4-cp315-cp315-win_amd64.whl", hash = "sha256:ceb3e879afac028f93d272c957814695dc5569e4904262dbee92f6c41bd5e4a3", size = 46264, upload-time = "2026-09-16T00:16:29.751Z" }, + { url = "https://files.pythonhosted.org/packages/65/10/fef04fbdcd44a4a163cb5ff5674599c6d6fdefd64a5a459438f9ad2ba042/propcache-0.5.4-cp315-cp315-win_arm64.whl", hash = "sha256:c83acbce9f2b5e3f5f5eda9e53d2001fed22fcdfef81274a9e02d8fd53b70a30", size = 43395, upload-time = "2026-09-16T00:16:31.5Z" }, + { url = "https://files.pythonhosted.org/packages/70/f6/7e2f4dab0b92ab46111bd48cee9ee1e5f519514c44e3779ede5358d7ada0/propcache-0.5.4-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:a5e8ef588c109725dc713ba69aadcac00a1ef90c2ce9c0a8c7075128f569f47f", size = 89825, upload-time = "2026-09-16T00:16:43.115Z" }, + { url = "https://files.pythonhosted.org/packages/9f/8b/dfeff925cb6ced97ede701d5c6a99998da963c6f2e06abbf879c9dac5b54/propcache-0.5.4-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:4d86476a935c88963d9b8e1a9a0d38188790e9622169bfbafa173046846709d3", size = 52159, upload-time = "2026-09-16T00:16:44.754Z" }, + { url = "https://files.pythonhosted.org/packages/24/6c/924c810be5b7cf218ef47e707cf06d34adb4e3f3a31e3c24c55c6d945a88/propcache-0.5.4-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:f5470694918830da62fac9e69133b53d23b736d7070e587b27a4a2be37e08e68", size = 53956, upload-time = "2026-09-16T00:16:46.762Z" }, + { url = "https://files.pythonhosted.org/packages/3f/b6/9ed0a5c939b58b6bed740a05b5d0f919f0b318d03284b4b6d81a0fe8a29a/propcache-0.5.4-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10ef33a68a61ce317e095fd2e202a592ea92392b90944a78c993f0d9a73ab06c", size = 295235, upload-time = "2026-09-16T00:16:48.577Z" }, + { url = "https://files.pythonhosted.org/packages/5a/eb/5ce886e902a2e781dddf110993d5329458a9b1a8626b876c65e5e25bf413/propcache-0.5.4-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5cacf3c9efd09df409dc33654dd077e1c245ba8fb747b0f0236ef41b7c49b589", size = 294463, upload-time = "2026-09-16T00:16:50.539Z" }, + { url = "https://files.pythonhosted.org/packages/f2/88/c98f49183ecd3e5b204a556f0ca47baa02c2206a500fe8c7ec1726297b0a/propcache-0.5.4-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:770e8209d018175fc0063936fa9583b6d27e88c5ad31543f3383d66080efdd62", size = 300081, upload-time = "2026-09-16T00:16:52.423Z" }, + { url = "https://files.pythonhosted.org/packages/27/0d/c5090f9e6f67cbc30a2b744c7bb0f8006dcba5ec1b0d82f866ae1cc7c5c4/propcache-0.5.4-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:03969626faf0783a592dfa17e28eac06018bd0b44dafae6943d53b92421a7f72", size = 285360, upload-time = "2026-09-16T00:16:54.141Z" }, + { url = "https://files.pythonhosted.org/packages/ac/9c/34a55396910583ed07926669ab309dde2213a2dec05a7e946bb90ad66908/propcache-0.5.4-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ef3b928d9c984322b5c44e6964d8dbc653da87d2d8ee1647fa6da43072e650a9", size = 248014, upload-time = "2026-09-16T00:16:56.062Z" }, + { url = "https://files.pythonhosted.org/packages/cd/b5/c0a142b656093ca397039dd3fe166cbb87c945712b534546514a24cd2611/propcache-0.5.4-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:7177c43eddf10a0893c4fec52ebb408fdcd7f7d63962caace9180d8f81b14ece", size = 280662, upload-time = "2026-09-16T00:16:58.044Z" }, + { url = "https://files.pythonhosted.org/packages/54/28/fab2809c2e337fe26becea9648e84d5cef46075c91b826acb13e4f9dd04e/propcache-0.5.4-cp315-cp315t-musllinux_1_2_armv7l.whl", hash = "sha256:420162a77f94eb1cf5ef7893f500016dabd548e73de956785a1dd899cc73006a", size = 266149, upload-time = "2026-09-16T00:16:59.702Z" }, + { url = "https://files.pythonhosted.org/packages/3a/11/7ddf336288b2678a5f054f8da2e2bd1a719f5d4b7de714d9c6bd588a2313/propcache-0.5.4-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:3eb2e820e8e2101407da93f17c57cbb7d225461955fc60105daaba14cd421ee2", size = 283097, upload-time = "2026-09-16T00:17:01.459Z" }, + { url = "https://files.pythonhosted.org/packages/1a/ae/351b1a5225f5473c411d9a612a229ae147cf0cf65c72ad838b87219ea8e8/propcache-0.5.4-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:13e52b6e0bde97dee98ab66552dbff2931649c96f1ac432eac299fe689ec373b", size = 248160, upload-time = "2026-09-16T00:17:03.298Z" }, + { url = "https://files.pythonhosted.org/packages/3f/d0/7f79f061e30d135bb615c9782c94a74652033d00b49254edbbf35a9165a8/propcache-0.5.4-cp315-cp315t-musllinux_1_2_s390x.whl", hash = "sha256:12682126712ddc19b70ff819debbd279e58adf1f0c8f8f8138c18ade2044b284", size = 283036, upload-time = "2026-09-16T00:17:05.238Z" }, + { url = "https://files.pythonhosted.org/packages/53/3c/016f1cad8bf4c428d748cf399b2bac603026fbfd6966e6a5579b5c5b6956/propcache-0.5.4-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:3af0c8642b2da4815d86e631232ac8286e17644fad907c19508aa8e7cb4ba8ad", size = 279350, upload-time = "2026-09-16T00:17:06.881Z" }, + { url = "https://files.pythonhosted.org/packages/ea/60/d8f72cb24b412487ed4c397f539117d3b74c3c33dd32020e91fe00a958a8/propcache-0.5.4-cp315-cp315t-win32.whl", hash = "sha256:1df8d8561b21465c5dd56110a01caf897e026d065b4b84e98a488209094272ec", size = 45874, upload-time = "2026-09-16T00:17:08.567Z" }, + { url = "https://files.pythonhosted.org/packages/d4/ef/8bae0a316d406644450522f2f3d44a4e19632f5f3bb60d1d0e6c53842616/propcache-0.5.4-cp315-cp315t-win_amd64.whl", hash = "sha256:02c0a34f16889cf800f10f0247a564d8ce6eeab6ffcd7c87198f769067eb8432", size = 48574, upload-time = "2026-09-16T00:17:10.077Z" }, + { url = "https://files.pythonhosted.org/packages/57/be/bcc053f66a97355683884b448198e79580fae8e8fa4d96b9bb01614e9913/propcache-0.5.4-cp315-cp315t-win_arm64.whl", hash = "sha256:dc4242ca653c9b30ab51c5f8193323e7bc0928f897ee9103201e59a43abcb72e", size = 45625, upload-time = "2026-09-16T00:17:11.377Z" }, + { url = "https://files.pythonhosted.org/packages/f5/cd/785c64ed382f3f04201870267b02783f63b4678c2acfddc177a3ebcc2727/propcache-0.5.4-py3-none-any.whl", hash = "sha256:62c60aec739ed00124573cce1178138fd690c7676352d67a37328c1cf51d7468", size = 16338, upload-time = "2026-09-16T00:17:13.106Z" }, ] [[package]] -name = "iniconfig" -version = "2.3.0" +name = "protobuf" +version = "5.29.6" 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" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/57/394a763c103e0edf87f0938dafcd918d53b4c011dfc5c8ae80f3b0452dbb/protobuf-5.29.6.tar.gz", hash = "sha256:da9ee6a5424b6b30fd5e45c5ea663aef540ca95f9ad99d1e887e819cdf9b8723", size = 425623, upload-time = "2026-02-04T22:54:40.584Z" } 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" }, + { url = "https://files.pythonhosted.org/packages/d4/88/9ee58ff7863c479d6f8346686d4636dd4c415b0cbeed7a6a7d0617639c2a/protobuf-5.29.6-cp310-abi3-win32.whl", hash = "sha256:62e8a3114992c7c647bce37dcc93647575fc52d50e48de30c6fcb28a6a291eb1", size = 423357, upload-time = "2026-02-04T22:54:25.805Z" }, + { url = "https://files.pythonhosted.org/packages/1c/66/2dc736a4d576847134fb6d80bd995c569b13cdc7b815d669050bf0ce2d2c/protobuf-5.29.6-cp310-abi3-win_amd64.whl", hash = "sha256:7e6ad413275be172f67fdee0f43484b6de5a904cc1c3ea9804cb6fe2ff366eda", size = 435175, upload-time = "2026-02-04T22:54:28.592Z" }, + { url = "https://files.pythonhosted.org/packages/06/db/49b05966fd208ae3f44dcd33837b6243b4915c57561d730a43f881f24dea/protobuf-5.29.6-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:b5a169e664b4057183a34bdc424540e86eea47560f3c123a0d64de4e137f9269", size = 418619, upload-time = "2026-02-04T22:54:30.266Z" }, + { url = "https://files.pythonhosted.org/packages/b7/d7/48cbf6b0c3c39761e47a99cb483405f0fde2be22cf00d71ef316ce52b458/protobuf-5.29.6-cp38-abi3-manylinux2014_aarch64.whl", hash = "sha256:a8866b2cff111f0f863c1b3b9e7572dc7eaea23a7fae27f6fc613304046483e6", size = 320284, upload-time = "2026-02-04T22:54:31.782Z" }, + { url = "https://files.pythonhosted.org/packages/e3/dd/cadd6ec43069247d91f6345fa7a0d2858bef6af366dbd7ba8f05d2c77d3b/protobuf-5.29.6-cp38-abi3-manylinux2014_x86_64.whl", hash = "sha256:e3387f44798ac1106af0233c04fb8abf543772ff241169946f698b3a9a3d3ab9", size = 320478, upload-time = "2026-02-04T22:54:32.909Z" }, + { url = "https://files.pythonhosted.org/packages/5a/cb/e3065b447186cb70aa65acc70c86baf482d82bf75625bf5a2c4f6919c6a3/protobuf-5.29.6-py3-none-any.whl", hash = "sha256:6b9edb641441b2da9fa8f428760fc136a49cf97a52076010cf22a2ff73438a86", size = 173126, upload-time = "2026-02-04T22:54:39.462Z" }, ] [[package]] -name = "packaging" -version = "26.3" +name = "pyarrow" +version = "25.0.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7d/fa/3944b40b07da9ce895c0e6303a5ab7d53da063554f534556b134a54d6093/packaging-26.3.tar.gz", hash = "sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79", size = 313412, upload-time = "2026-08-04T18:15:28.737Z" } +sdist = { url = "https://files.pythonhosted.org/packages/3d/e3/27f57f80141379d60defe6703eb50a707325706f07fedfd1312c7a751995/pyarrow-25.0.1.tar.gz", hash = "sha256:9150a83248bfed9813ea3c3af74c3856c1984d444aa28e58bf7733b9750ddf6a", size = 1201653, upload-time = "2026-08-10T12:40:53.904Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/63/34/ba1c580383c9eada3711951fef0795c80b829a078d72188184bcab9dd527/packaging-26.3-py3-none-any.whl", hash = "sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c", size = 129956, upload-time = "2026-08-04T18:15:27.159Z" }, + { url = "https://files.pythonhosted.org/packages/ee/8b/0d23b47702fcfe8b3618d5292035099675c5a1c48258932350c08020f7b5/pyarrow-25.0.1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:51093dd9e10325fbdb3c10a2ae7c4806e5c822d94e74ae4938b26524a3323fee", size = 35946180, upload-time = "2026-08-10T12:37:18.934Z" }, + { url = "https://files.pythonhosted.org/packages/d8/17/707d17a5476c55a9541fde0db8213ac30979a792864d72415f176ba50c45/pyarrow-25.0.1-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:eb6203482ff3746a5632303a7279ae0b5a304c46985b49ed1378cb350ea6728d", size = 37644787, upload-time = "2026-08-10T12:37:25.795Z" }, + { url = "https://files.pythonhosted.org/packages/c1/b2/cdc98ecf1a6408280bc3a6a07054cdd99a3f4670acc0545d383ce113e87d/pyarrow-25.0.1-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:880523be3d29efcf83d3998835d206118ccf35e3871dbd2fb60408cf6b007a80", size = 46834633, upload-time = "2026-08-10T12:37:33.604Z" }, + { url = "https://files.pythonhosted.org/packages/c8/6e/d3fafc41f378b2c65be43b827798c0fae42049a641c8526633ed3eb573e2/pyarrow-25.0.1-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:25f8720bf6387d5dc2ebd2622112de630760419e4b66134405dd24110d15f37e", size = 50065507, upload-time = "2026-08-10T12:37:40.565Z" }, + { url = "https://files.pythonhosted.org/packages/d5/12/8d0698954b8c3001844a898e0a6900bebe83d7ee40c11195174c5122f324/pyarrow-25.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4facd65742a024a4a366328a1d2292062d72d6e023c1b7dda8d4c37544933a25", size = 49955690, upload-time = "2026-08-10T12:37:46.644Z" }, + { url = "https://files.pythonhosted.org/packages/d3/0b/1ecb936ac6409e90a34d58eea1c7cec09a9ae6d2141b9e49ad01a2b1ea47/pyarrow-25.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:aa0559502e1cd6254d6814614085dd9c5a3dd0419362978a936a3f68a9e5c3df", size = 53128198, upload-time = "2026-08-10T12:37:52.531Z" }, + { url = "https://files.pythonhosted.org/packages/8e/1c/5236033550633c9b7377b2a53660b2bbb06cb06dc09c4356332d67643ca1/pyarrow-25.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:62cd0d785b8aa6675ee355f9fc02252a340f4441257c42674937826fd7594325", size = 27857263, upload-time = "2026-08-10T12:37:56.943Z" }, + { url = "https://files.pythonhosted.org/packages/a6/e2/9ab15b88cbfac28e16419ce5439ec29234c5172cb8259301b4ba639bdec0/pyarrow-25.0.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:df961f2e7ae9cf496459259d798652c70625f6c080650d6952f8c04053c58ee9", size = 35861559, upload-time = "2026-08-10T12:38:02.567Z" }, + { url = "https://files.pythonhosted.org/packages/58/79/a0036dbe1eabe1f73127427342f1d99982584c4a2cde2651d6c93499c6f6/pyarrow-25.0.1-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:cc4aa407fde9fc660be3939e49ea31f50f3e9fec17c0ec63159f7711edd3efc9", size = 37628383, upload-time = "2026-08-10T12:38:09.083Z" }, + { url = "https://files.pythonhosted.org/packages/13/49/d93a57d375f4bf0cf82913dd6bb54acafde83dd993be2282c81ac5616cad/pyarrow-25.0.1-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:4340f0ba6c1d2e13f21658de1d7c662ca2545018568d0030a1e9afca159d87e3", size = 46820190, upload-time = "2026-08-10T12:38:15.458Z" }, + { url = "https://files.pythonhosted.org/packages/60/c9/711ca85d79f1ec98f29a5eae2b051e25b4ecec5de3e3c0e2d5c5dcb15664/pyarrow-25.0.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:5389cdf79447ed1515c9e31620e6e1e2302249564d603f2ad727d4f6d313e4c3", size = 50102437, upload-time = "2026-08-10T12:38:22.487Z" }, + { url = "https://files.pythonhosted.org/packages/80/53/8fb8359ff17cfb6263a1cf3ebf7caec9fe197de118719e84fcb1d0618026/pyarrow-25.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d51592cb7561e87877c506113e7adbf1342ab579e6c21f0ef44b8ba41cb74c80", size = 49942424, upload-time = "2026-08-10T12:38:28.755Z" }, + { url = "https://files.pythonhosted.org/packages/e8/83/4e5ae02a9341571b18a6fca380ac7a58ce6ddae7ab3c060208c0a1e79f02/pyarrow-25.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6109c94d8b9f3b17a041daca16cacb2f651ad8f1ef70a4232c2c0f37a23da2a8", size = 53144206, upload-time = "2026-08-10T12:38:34.862Z" }, + { url = "https://files.pythonhosted.org/packages/65/ee/197cbf47e49f83e6ebeb946a5259a48a638dea27ac774db42fe78022179d/pyarrow-25.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:8858d7bfc22e3f51529aeaa4077225029724623e4595dc9eff8c793935c34140", size = 27953934, upload-time = "2026-08-10T12:38:39.808Z" }, + { url = "https://files.pythonhosted.org/packages/cc/8d/8f271a7a034c834910ec925d56fa4b29733b1380f5289419f5aaa3b02777/pyarrow-25.0.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:c7c534ec03c358a76ea3e505e74c1b6aef290af90c444dfd092dbfe23e755b85", size = 35855328, upload-time = "2026-08-10T12:38:45.489Z" }, + { url = "https://files.pythonhosted.org/packages/d2/cd/5bac242f4e841b9971d5eb94fdfe2577e2b70be983e27401e72055786037/pyarrow-25.0.1-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:dda9470024204d7bbf2042b47c6e8a0e47a3eeb8e34405882dfaea6577e0c153", size = 37622415, upload-time = "2026-08-10T12:38:51.107Z" }, + { url = "https://files.pythonhosted.org/packages/63/1f/96d03b4e1506524f7087adb0fd6b2f69f0c9c7aaff1ec36d8030082e15a5/pyarrow-25.0.1-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:44a9120ce5bd81936b8ab9a88076e3fd47c2c6838e0e43630fed83626aca81d9", size = 46813813, upload-time = "2026-08-10T12:38:57.773Z" }, + { url = "https://files.pythonhosted.org/packages/98/d6/33a411115b61dbfc16ad6ad73e71730f6fea654ee3667673bc53ab0e2fe7/pyarrow-25.0.1-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:0befcf816e45a1af33ac775a9970b749e4868a230c7372f0ae5e932bee27039f", size = 50104452, upload-time = "2026-08-10T12:39:04.579Z" }, + { url = "https://files.pythonhosted.org/packages/33/ae/b1b97c9ca87f9f9ddbb5230c798df94eccce61bd79b9b45458c69a478588/pyarrow-25.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3f89685964f46e4216103c75483aac0c0692a5f72212d7ca835adba5ede56ce3", size = 49951343, upload-time = "2026-08-10T12:39:11.8Z" }, + { url = "https://files.pythonhosted.org/packages/98/9e/a112df5cfd5a68cb1d9fc31cfe38c28d5aec9f10865ce37ecef2e4450873/pyarrow-25.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6943e2fe7954d29d84de45d29d34c8dc36ce96570e67d89aa9976e650a4a9138", size = 53144784, upload-time = "2026-08-10T12:39:20.503Z" }, + { url = "https://files.pythonhosted.org/packages/31/24/97e8bd98f1e3b07e2ba08bcdff690674fbe16d69a7d2712cc3884665e615/pyarrow-25.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:31e49a7888fcdf3a835da33ae777f6bb9a866334e5a789282fc26dcf426f7f15", size = 27870159, upload-time = "2026-08-10T12:39:26.161Z" }, + { url = "https://files.pythonhosted.org/packages/36/4c/b525824ad3094076919273cd97db61fb3d78252dee76fa3b8dc8f76774aa/pyarrow-25.0.1-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:bf0b672390cdcb640d7288f96b826d71ff4e9abb254a86c89890baf51a29cee6", size = 35885255, upload-time = "2026-08-10T12:39:32.366Z" }, + { url = "https://files.pythonhosted.org/packages/08/62/448bb0e940de41aec31d1a956e63ad9c54afdf122a103cc3ab20c2a3ce33/pyarrow-25.0.1-cp314-cp314-macosx_12_0_x86_64.whl", hash = "sha256:38a9a4b4b9613380e200641891495a56c3d5a98a092db4a870af9975e220471d", size = 37644461, upload-time = "2026-08-10T12:39:38.142Z" }, + { url = "https://files.pythonhosted.org/packages/6e/9a/13587e38bd4806fd218f50fd13b8903fab60588a699ff0c406372e5b4043/pyarrow-25.0.1-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:0b726ad7e7b669be982b0c71c07fe4b037d654354130da79a7902a669e93a66b", size = 46877146, upload-time = "2026-08-10T12:39:43.722Z" }, + { url = "https://files.pythonhosted.org/packages/8d/61/1c5d1229fa21da4cff5365e41e57177aaac57c563c727f35419b8513d1c1/pyarrow-25.0.1-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:9171748cdf796972d85a4b60157c279913e242992e350c90c7450182a9838b2a", size = 50131616, upload-time = "2026-08-10T12:39:49.304Z" }, + { url = "https://files.pythonhosted.org/packages/43/20/291e1d65cc0b09aa19f03cf25cf51a2f5fa94b5db315178f2d254ed5cad4/pyarrow-25.0.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b7a296aac7a71fa0886c08e155ddb6c636a50013f801f6178daafa0f9e726188", size = 50008879, upload-time = "2026-08-10T12:39:56.891Z" }, + { url = "https://files.pythonhosted.org/packages/8b/7c/1b7c9ec28e76576337e4f97b31141c9a181b89b6d1d6221e9d8205621a58/pyarrow-25.0.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:0fe7c8b6c03969b49c8c66182e4a18e3819ab92d07cfab5d8370c531b9369ef0", size = 53170864, upload-time = "2026-08-10T12:40:04.918Z" }, + { url = "https://files.pythonhosted.org/packages/b7/75/f3d789dc06011a765d14d86bda799cf72ac1d715b6a6edecaa0d73d95062/pyarrow-25.0.1-cp314-cp314-win_amd64.whl", hash = "sha256:f729cfdbd36fd99d543b67a914d2de044c84ebe45be8b34902b299b608c15c8f", size = 28620729, upload-time = "2026-08-10T12:40:51.41Z" }, + { url = "https://files.pythonhosted.org/packages/fc/05/647a8ee6f7c2662feb6921315617bc04dcd6034763fb61b1199720bf6162/pyarrow-25.0.1-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:59a2de54c0cbd954da861eee4d1d330f8e909c45b53455baef696380f2c55033", size = 36130288, upload-time = "2026-08-10T12:40:11.014Z" }, + { url = "https://files.pythonhosted.org/packages/93/f8/c9ee997554d7bea94520667dd1933f109ac1da3ee3556d2b49381e023484/pyarrow-25.0.1-cp314-cp314t-macosx_12_0_x86_64.whl", hash = "sha256:35935cd5de130aa5cf4dea052a63e6bf2e17006c35c3a468194242b9b2bf5956", size = 37762187, upload-time = "2026-08-10T12:40:16.592Z" }, + { url = "https://files.pythonhosted.org/packages/a2/08/a28c01c7fe9e96e8233ce2d13df1d402f4f999f848f51d2daacd6bb4c036/pyarrow-25.0.1-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:f3831aaa25c67a99f99dc8b05873cb9d64560390372e2aa197ce9dd4a3f06a44", size = 46888003, upload-time = "2026-08-10T12:40:23.242Z" }, + { url = "https://files.pythonhosted.org/packages/1b/b9/58612e977d28dc58c878448866838369ee8da2f1e7cc8ed2c84b952aafee/pyarrow-25.0.1-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:6a1fdfc6659b6b19022f2e50627fb5cf7156a66c46bf4299379955cbe742382a", size = 50079036, upload-time = "2026-08-10T12:40:29.169Z" }, + { url = "https://files.pythonhosted.org/packages/72/13/66e1402dcc860e1dc2760b1e0292c9a569b62b3bccab69def1b3e907d006/pyarrow-25.0.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:169d3429d5be7c752125890620f75a60776d38b0035eddae939651640822332e", size = 50040226, upload-time = "2026-08-10T12:40:35.186Z" }, + { url = "https://files.pythonhosted.org/packages/78/10/3f1a5497a7ef732ab0f03ecca3e66d89d9c0f57fdc61b4794c456b781f01/pyarrow-25.0.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:119297a6dc197e45d9c6d4415f7814a67ffa36c180d26f68c154c58067ae782d", size = 53149035, upload-time = "2026-08-10T12:40:41.454Z" }, + { url = "https://files.pythonhosted.org/packages/93/c0/37d4a7e8e2f7a6076283673d5298018ca26478b934c6ee369e10505ab32c/pyarrow-25.0.1-cp314-cp314t-win_amd64.whl", hash = "sha256:4288f27577352d608ca08553b0865e4a9b3aa14820c5d95b53337218d609835b", size = 28753071, upload-time = "2026-08-10T12:40:46.623Z" }, ] [[package]] -name = "pluggy" -version = "1.6.0" +name = "pycparser" +version = "3.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" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" } 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" }, + { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" }, ] [[package]] name = "pydantic" -version = "2.13.5" +version = "2.12.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "annotated-types" }, @@ -84,111 +1724,120 @@ dependencies = [ { name = "typing-extensions" }, { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/53/ef/fc4f868f4e2cee79f863883abffceff107875f569b848507319842d2a681/pydantic-2.13.5.tar.gz", hash = "sha256:51a9c5f7b2f8e636f04c6cada605d9b6a3bf1348fdf945a3d8869b19bba0ee08", size = 845750, upload-time = "2026-08-28T14:04:00.916Z" } +sdist = { url = "https://files.pythonhosted.org/packages/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591, upload-time = "2025-11-26T15:11:46.471Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/eb/47/c95ffc2009878c7aac0c5e08528022dcb885933252a88b5f170058014464/pydantic-2.13.5-py3-none-any.whl", hash = "sha256:346a034f080da3755d8e9cb5e00e8b07de1d39e4f6e2c87d8ab7cafa0b269a73", size = 472589, upload-time = "2026-08-28T14:03:59.136Z" }, + { url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580, upload-time = "2025-11-26T15:11:44.605Z" }, ] [[package]] name = "pydantic-core" -version = "2.46.5" +version = "2.41.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/af/f9/8a06bea35ef8daf588f707784c973a7046e0034c8d8cfb08828eeffb8b75/pydantic_core-2.46.5.tar.gz", hash = "sha256:10416c15b8839ecc4ef4d0885da76da6fd0f67333a0eb8aff6d93c4b8f2910fc", size = 472262, upload-time = "2026-08-28T10:01:31.677Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a2/b6/81d2d19ea0be2c03664381b59f65fa72fc7969decedae00bc2c4ad835708/pydantic_core-2.46.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a1dee1b804ff4d11c663636cf15d2ea47e9f79cd56c033fb1cbf08924842a48f", size = 2074737, upload-time = "2026-08-28T09:57:57.711Z" }, - { url = "https://files.pythonhosted.org/packages/0c/18/b70da8300e292df4099684ea11b1958043580d2f50d2dc8bf7e542bdd84a/pydantic_core-2.46.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d625a186a65201c23a9e3b8ed9c47e90a026e03256608cc91851c6709096844f", size = 1921751, upload-time = "2026-08-28T09:57:59.265Z" }, - { url = "https://files.pythonhosted.org/packages/e7/1a/0d590341b6ffa4b4aca83508e6b8db4761aaeacfc15a25ca3815876d4797/pydantic_core-2.46.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4f8507560a9284e1370bb048ed4282012fbef4e8d109875b95e884d228552061", size = 1948231, upload-time = "2026-08-28T09:58:00.678Z" }, - { url = "https://files.pythonhosted.org/packages/7d/1d/02eb35761c51f2f7b1b042d6ab4cda6600f0c8c88a2243b3f734376201e5/pydantic_core-2.46.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5f93c5fe914d75fbec9a49209b00da5f08e9e467d69da2b1510c81940cfd10be", size = 2020708, upload-time = "2026-08-28T09:58:02.267Z" }, - { url = "https://files.pythonhosted.org/packages/4a/ea/f86073830e35d508cc8ddf9c3d9e6e6840fcb88d34bf726b0b4710186f27/pydantic_core-2.46.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aca6c767f552b21b10f774aeac128e828eafb796adfa1b666a18bf6321453c3a", size = 2194914, upload-time = "2026-08-28T09:58:03.934Z" }, - { url = "https://files.pythonhosted.org/packages/bb/d7/fc36240d7791ce90939e51608568c33bfdae26202016f9770c229a487d86/pydantic_core-2.46.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:701b2e04b560eeb4bddf7a25ab8ca476176e34fdbd9a0e18196f0d12d4685f0b", size = 2235622, upload-time = "2026-08-28T09:58:05.516Z" }, - { url = "https://files.pythonhosted.org/packages/cf/bc/3fa2d76b83162820a17da7f645b28d1cba99fc8e1e5fc6517067ec450fa1/pydantic_core-2.46.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49776eab08766a08dfff7012f8b422dcd7e25e43b316eedf0477c24fcfa84b7c", size = 2062091, upload-time = "2026-08-28T09:58:07.135Z" }, - { url = "https://files.pythonhosted.org/packages/ab/9a/095d557bb492c90cd8a70a6dd048bf793d433d03d86c81c11e912e4cd049/pydantic_core-2.46.5-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:a2468d93d181667a7abd66e1b64bb9f76f361b0fef8faddf687456453576f5ee", size = 2089904, upload-time = "2026-08-28T09:58:08.814Z" }, - { url = "https://files.pythonhosted.org/packages/24/98/7b76b1ad10a19a617a52aaa1d80e159115af939b095e86f8e756fd52e0df/pydantic_core-2.46.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:53feb344243bb9510a9dec7bf3cf1b64d88a98af5dc7872a5160465f8b198c8e", size = 2132244, upload-time = "2026-08-28T09:58:10.435Z" }, - { url = "https://files.pythonhosted.org/packages/20/32/7d6ca365fadba186a0c8f85de1a701663bce81efd309d9479be58687622f/pydantic_core-2.46.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:cd5214352ae68f3b5e9af7768bdc5253695ee069675db3480518420b3be881f2", size = 2143901, upload-time = "2026-08-28T09:58:12.033Z" }, - { url = "https://files.pythonhosted.org/packages/f8/09/eb9a6aa57f22fd1541a9c0aa2a1f3aeef3ec65347d33e10a6da2f43e0ee9/pydantic_core-2.46.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:9432f3598db432cb51c5b37fdbf29a60fcccc79e30d37a05022776a6bc4ab689", size = 2299425, upload-time = "2026-08-28T09:58:13.614Z" }, - { url = "https://files.pythonhosted.org/packages/8a/f9/548a5bb9d4ba8cd26e26daf48052236f6b38bb61e7b7241fbc3c995719eb/pydantic_core-2.46.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:8feeac04b5794e513e710af2f9c87d49f31a6dc47967bb264a1fed61a8989bec", size = 2318566, upload-time = "2026-08-28T09:58:15.199Z" }, - { url = "https://files.pythonhosted.org/packages/4a/20/06454d18834c02c406c9133f1a3b485305fd9ee984f9636c2f730bef6a9d/pydantic_core-2.46.5-cp311-cp311-win32.whl", hash = "sha256:892a881d5f68c2b9ea304b7a6c2c60d9343df578a311b0f86b94bc8f1ffe8129", size = 1954258, upload-time = "2026-08-28T09:58:16.813Z" }, - { url = "https://files.pythonhosted.org/packages/9e/c2/718b9deb4b72453b5d8c7447a3b14cb77bef36917ef5f514e0948a4096a0/pydantic_core-2.46.5-cp311-cp311-win_amd64.whl", hash = "sha256:40375c2d05acec10323e45dfe2077ac44bc74659008614af5069034e2cfc781c", size = 2041030, upload-time = "2026-08-28T09:58:18.288Z" }, - { url = "https://files.pythonhosted.org/packages/67/ea/c1d1a5b72d6e1ff7f377a4d9199f6591f095beb5b409a8a5d89f7238d939/pydantic_core-2.46.5-cp311-cp311-win_arm64.whl", hash = "sha256:28a6a556cd3b6066bea827857f9d9cce027c96f776e512f544a581f9e42161f8", size = 2009234, upload-time = "2026-08-28T09:58:19.929Z" }, - { url = "https://files.pythonhosted.org/packages/82/3f/76358795aa7a8c6d4f36e2cb828ad1c90ee118e1393a9281664f5aade9d4/pydantic_core-2.46.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:b9fe6fb92520e3fd61f2e49000b6911b188824f089b75973ea06d6267f0b476d", size = 2076516, upload-time = "2026-08-28T09:58:21.576Z" }, - { url = "https://files.pythonhosted.org/packages/db/50/26b091836076ce4cb2fac264186936acc069e0595772cfd02a563bc4761a/pydantic_core-2.46.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a39ac25a9a2fa4072efdb429833c4a4c8009a51ff9eea3eeae131713cd27991e", size = 1922874, upload-time = "2026-08-28T09:58:23.766Z" }, - { url = "https://files.pythonhosted.org/packages/09/f0/2a8ce3849e299d44e2d2c196b6082643a3235565a735cb51db7a6261f614/pydantic_core-2.46.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4fdc8b93a41521988916eeaa271173fcca7fa0803d62f87675aac8dcec1c8e29", size = 1951772, upload-time = "2026-08-28T09:58:25.435Z" }, - { url = "https://files.pythonhosted.org/packages/87/46/ac0dc8bdd9e6048183a14eb127764e7ad9240021c17513074a4711b0e31e/pydantic_core-2.46.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b98134087d9de723658d17a42c7d0da8d6e2ef08015dee7dc93889047315f5e4", size = 2031832, upload-time = "2026-08-28T09:58:27.102Z" }, - { url = "https://files.pythonhosted.org/packages/c4/c2/339de5bef7be36301a2231eaa52e62163742c2281f11b5f4892bc79785cd/pydantic_core-2.46.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e652ab17569c94bff5475520f907b7148b8c24036a8ebbe5cf7cf7493d28579a", size = 2208645, upload-time = "2026-08-28T09:58:28.948Z" }, - { url = "https://files.pythonhosted.org/packages/7b/a0/9ff22b797724262da14427abaed4dd1d864a139693fc5e7809114376a716/pydantic_core-2.46.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d925f3d9afd05a8c0fb3a1031463a8d59ebe5e2afad297e29c78be19e13b4e62", size = 2265935, upload-time = "2026-08-28T09:58:30.625Z" }, - { url = "https://files.pythonhosted.org/packages/c0/a4/eb9409ec0736e50aa70a412f16c204ed149516846912f7e6724d4c73ee53/pydantic_core-2.46.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0fc5be0abd4a407e200d844b404e33639a554e7bd0d448e7b9ae181be4789ac2", size = 2066284, upload-time = "2026-08-28T09:58:32.289Z" }, - { url = "https://files.pythonhosted.org/packages/c0/02/7f6156ffc926857f1c37c07d9a388682865a81830ab6a1b637082c25e399/pydantic_core-2.46.5-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:816ff0a6550ffc06c098ccd2e0698600f9aa7da192a79eaa6f9af504a35db869", size = 2105889, upload-time = "2026-08-28T09:58:33.986Z" }, - { url = "https://files.pythonhosted.org/packages/92/b1/e781d357ebe09fc929f995700f1b3503e8897f1cece183ecb1300d4d67e9/pydantic_core-2.46.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c7ea57fc63aa7da93a1bd2d644e6577befae10c52c4e36377635eea1056a74f5", size = 2158006, upload-time = "2026-08-28T09:58:35.647Z" }, - { url = "https://files.pythonhosted.org/packages/70/0a/644597d84ab400e50609c192120b85c9681c22d3a20461b9060a79be0a7a/pydantic_core-2.46.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:efd62a42486f1bda5d24cb4f63d15a3c7768375fe83d36f9417b4ad7a2fb20b3", size = 2158408, upload-time = "2026-08-28T09:58:37.38Z" }, - { url = "https://files.pythonhosted.org/packages/1e/ee/ca3b7b3a4b3769ffe9ce9432a7c9be755de9593a46d3b0d54d0409323e44/pydantic_core-2.46.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:2bc9419666990c06d7397831f2126a1ecc3594aaa3ff7de5bf2d066802f4e07b", size = 2309609, upload-time = "2026-08-28T09:58:39.22Z" }, - { url = "https://files.pythonhosted.org/packages/ce/52/39fa1f451486019524ca685020390e7ca351832fd874530ba30c8628e6dc/pydantic_core-2.46.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:18a09e1e1011b462f2e32774f25859ef1223d5c2b0546a633cf56654710721e0", size = 2342618, upload-time = "2026-08-28T09:58:40.89Z" }, - { url = "https://files.pythonhosted.org/packages/81/5e/468fc630568c61dcef3cd47ad32ffbeed9af643f49208d1ea86ab4f890c4/pydantic_core-2.46.5-cp312-cp312-win32.whl", hash = "sha256:5cb482e9e84c851f4e623fe4acc1ced89168cf1fe18f7089db4548c8f5bbb65b", size = 1939475, upload-time = "2026-08-28T09:58:42.591Z" }, - { url = "https://files.pythonhosted.org/packages/cf/c9/4c19f41b84cf6b622a72fbeed7665b25d47a187d68d47d0d430c07f23268/pydantic_core-2.46.5-cp312-cp312-win_amd64.whl", hash = "sha256:5e81740c09e310f5aa5cbd3e434a01c154d4bef93241c7877b39f211d2b78ba8", size = 2043140, upload-time = "2026-08-28T09:58:44.272Z" }, - { url = "https://files.pythonhosted.org/packages/af/dd/0c1a050299147c746e5256db16d645ab5efd4f78c59937d581a0524e74a2/pydantic_core-2.46.5-cp312-cp312-win_arm64.whl", hash = "sha256:f7b0ec93a2893de856652154d73b7ba622f26fa97726487dcac373de5f4c6084", size = 1997729, upload-time = "2026-08-28T09:58:46.13Z" }, - { url = "https://files.pythonhosted.org/packages/f5/37/5abe39a8372a61d3dc3c1338fc504281c01b32fdb3169cd7187153b56d3e/pydantic_core-2.46.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:b7ca9034437b6022f941f4857459562ee00a560b97e7cce8a0ec5a74fc6766e0", size = 2075885, upload-time = "2026-08-28T09:58:47.856Z" }, - { url = "https://files.pythonhosted.org/packages/21/43/6323b1f8b217780454c61304bcd2b38ae4762f50754414124603ccc90bb2/pydantic_core-2.46.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f332f0e72a5a0400141f830744e141bf9f97917878dbe968669e8a7fefea78ff", size = 1922768, upload-time = "2026-08-28T09:58:49.58Z" }, - { url = "https://files.pythonhosted.org/packages/0f/a3/c05ca796e1197618a774b01e596aeedfefc2f7d8c01ae3054e910b120e8a/pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:193375f3548919d3f0b60936ca113ada3e38f264f91b9b8e0508efaad57be931", size = 1951241, upload-time = "2026-08-28T09:58:51.511Z" }, - { url = "https://files.pythonhosted.org/packages/68/32/33bc39ac705c52cffc908e8389f9754fdb208aea5c69cceddf4eb3ce99af/pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:79bdfa52f843137045b2d081cc05c120ba6665d29b7559c2c47690906f39279f", size = 2031975, upload-time = "2026-08-28T09:58:53.166Z" }, - { url = "https://files.pythonhosted.org/packages/b0/70/2333e885c0f6a67bc105c5916965dac9b57f2718ee20d81d1a06a4ebdc13/pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:24922243639cbdac66c75fcb6fd6495a9cb52b213d62f9a0d16f0310b1ff8038", size = 2208542, upload-time = "2026-08-28T09:58:55.017Z" }, - { url = "https://files.pythonhosted.org/packages/f7/ea/296debfb4264207bbda5936133892e027c0a58875ad53ebd512fba8ec3a2/pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c76fe65e607be28c7fd4d56fc3c42b1583aa058ce3408b7ad0fd540171d31f9f", size = 2264692, upload-time = "2026-08-28T09:58:56.767Z" }, - { url = "https://files.pythonhosted.org/packages/d3/f2/9e4de77a6271e07a76d2d58b11c091a979c191ed2939bf80067568b369d2/pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f7b393a8b3da82f5c1fc0751e6d01ac6c55b93c18226a60bdfba4a724efafd1", size = 2066633, upload-time = "2026-08-28T09:58:58.531Z" }, - { url = "https://files.pythonhosted.org/packages/8d/db/f9e9d0c97445987b2084823d5c240de88087338f04fc2cfaa2df186b8049/pydantic_core-2.46.5-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:7ac031912d54f3d83ef3b3eb98dfabc1608802e2202263d25957eeed40b94761", size = 2105235, upload-time = "2026-08-28T09:59:00.421Z" }, - { url = "https://files.pythonhosted.org/packages/07/c5/79169b047b3b2c3e99e04bc76372af9637e0bf6db638274fa927df96369e/pydantic_core-2.46.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:837b396ca3d7b74091ca623f6cbd8351bd42d670a79c2683e79fb089f06a2de5", size = 2157367, upload-time = "2026-08-28T09:59:02.442Z" }, - { url = "https://files.pythonhosted.org/packages/26/b5/ba6057afb7c291bd449f51b867f95aef2072941c4ce4e5c31d6ffd132d3b/pydantic_core-2.46.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:5ee239d575f80b08eca11f6e20f90c4c695de7825c67eefe6091fbf20dda648e", size = 2158420, upload-time = "2026-08-28T09:59:04.2Z" }, - { url = "https://files.pythonhosted.org/packages/6e/28/2057abecaafdc22912afa819603a51f0a62d40643b7c4871c51721fea9be/pydantic_core-2.46.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:e80675d75ae2cd14372cb65cad5400d9347a3d3f6c13000183f22dfd027283ed", size = 2309588, upload-time = "2026-08-28T09:59:06.048Z" }, - { url = "https://files.pythonhosted.org/packages/71/9d/881156dc404e27479c4246128d73538464cab4a239bec61995e227644c30/pydantic_core-2.46.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:9c4b71f10dd532fb7a5cbc8f58707779e64f03a258c2bf8bfbaecfcd9970b519", size = 2341866, upload-time = "2026-08-28T09:59:08.539Z" }, - { url = "https://files.pythonhosted.org/packages/5a/38/d66f443a259f84d13babdceae568e572b0ed26da17ca5d0a649ebb110a67/pydantic_core-2.46.5-cp313-cp313-win32.whl", hash = "sha256:97bf8de4d541598c94a59344eeb988a94c08ff76b5723c41f6567ec18c7892ea", size = 1938580, upload-time = "2026-08-28T09:59:10.402Z" }, - { url = "https://files.pythonhosted.org/packages/2c/1e/1d5371213f4cc9a7ed70c0bfcc7911de22311ee99a662a56077d7292d2ac/pydantic_core-2.46.5-cp313-cp313-win_amd64.whl", hash = "sha256:15f4a94963c95accac15b7b657bb177d3ad82bb90b0d0526d9a9b85079925db5", size = 2041980, upload-time = "2026-08-28T09:59:12.396Z" }, - { url = "https://files.pythonhosted.org/packages/5a/48/4222d90b1c67568bace4dec6dca6271449c66de3595d72b6d098f5fde597/pydantic_core-2.46.5-cp313-cp313-win_arm64.whl", hash = "sha256:d22a945598fb91236b4dd793a6e42e4f3dd7740bb5aace5ebd7d4c08d13bb575", size = 1997213, upload-time = "2026-08-28T09:59:14.245Z" }, - { url = "https://files.pythonhosted.org/packages/8e/8a/14596f2a8367da50cf7cbac48169ee5d9c8e11d486a3b527082384630c72/pydantic_core-2.46.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:c1c43ad4339643d70ebb8124e1305a7dab423001eff58bb41a0f731adbc98355", size = 2074081, upload-time = "2026-08-28T09:59:16.141Z" }, - { url = "https://files.pythonhosted.org/packages/ae/d5/d8a4eb6d6c7f66b91dd37c576d76e9e60fba900caf5372c17bcf949febc2/pydantic_core-2.46.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1a353f84de772f423b5ffb11d7ae352fbbef0f446f3c0b0af0f8236d7233606e", size = 1920497, upload-time = "2026-08-28T09:59:18.065Z" }, - { url = "https://files.pythonhosted.org/packages/8e/26/092079428f86e927e030b2c0ced87df69dbb1c875cdeaa67bf42ea2be746/pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5086029a57366b8cf81b130a43908738095c270c21a8d7f0e8bdfdb89718e2f3", size = 1952130, upload-time = "2026-08-28T09:59:20.476Z" }, - { url = "https://files.pythonhosted.org/packages/08/c3/8ec0e290a9ebaebd64047bf5fda94be835c6b1551b02437e4b76778fbcd7/pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:46c25dda9d092a06c08db76ffe0a197107904d0dfac653f7d5306bbcd6d6119c", size = 2026371, upload-time = "2026-08-28T09:59:22.227Z" }, - { url = "https://files.pythonhosted.org/packages/01/72/4fd20ad520fb8da0157f95b27a7eb05a72790ef08138e7701ac972c342ea/pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:37ea7b83c935e5b0d68c9449b82651accf78a10828b2c02b2f2d9e9496446c21", size = 2202822, upload-time = "2026-08-28T09:59:24.277Z" }, - { url = "https://files.pythonhosted.org/packages/31/b0/d16e0771206b29314f0d52198b720be21e8a99ab2bf11e3bc0d7c9cebdff/pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e64e88d5585bea9ce95861079de72006c7fa6d3df4e3a3b65ba31eb979c15c9f", size = 2262756, upload-time = "2026-08-28T09:59:26.608Z" }, - { url = "https://files.pythonhosted.org/packages/2c/9b/59634b7ac631c63b2a37760eb6943af3e29573d6b59a4abc5e7f019d4cee/pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:54d510bac3ee52247af28ed4bb18a1e799f040ac60fd2bf5ccd4c92f1fbe786f", size = 2068352, upload-time = "2026-08-28T09:59:29.044Z" }, - { url = "https://files.pythonhosted.org/packages/08/7c/570abb1ad2155348dc754ea91be22e5aaa18eb6d69a6068f7c6f2679a6ed/pydantic_core-2.46.5-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:a2a5e1d0ff29adddc9f6d6821a66302e4493f8ca898b715b6b1182c2c201ea0a", size = 2104777, upload-time = "2026-08-28T09:59:30.95Z" }, - { url = "https://files.pythonhosted.org/packages/8e/25/5bf74adc65a1ac5b7be3f6cb0bcb5433615c1598a801c19d830d84c98ded/pydantic_core-2.46.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:03b9666e41e35d8909852ba191a0607520f81b74eaf12ccf8737005dbb313821", size = 2156312, upload-time = "2026-08-28T09:59:32.604Z" }, - { url = "https://files.pythonhosted.org/packages/90/6a/2ef38830675e050121040618135564ed56b860b45433b02d9b4ebece46f3/pydantic_core-2.46.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:a91c17edf6eea2402cb5457b4c89e99bc5ed1004aa34c4adf1d4258c1a5c22c2", size = 2150067, upload-time = "2026-08-28T09:59:34.453Z" }, - { url = "https://files.pythonhosted.org/packages/90/ef/a7dbb03a14a64c2a4621f989c615ed9a892535a6cad938fc27079f919d80/pydantic_core-2.46.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b49924c73a235e969511bf2aabdff3beebf9820931f646c80274d5d780010c47", size = 2304516, upload-time = "2026-08-28T09:59:36.194Z" }, - { url = "https://files.pythonhosted.org/packages/68/f8/6bb4c4b80e8a6fde1904c64a51c62a1d04fcdfa3ea521a66b2ddefa1d885/pydantic_core-2.46.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:2cbd9a5eff05e51c447c34dfa4632145b26b09120cf04bd0c871e44c1a5e1c9a", size = 2335223, upload-time = "2026-08-28T09:59:37.931Z" }, - { url = "https://files.pythonhosted.org/packages/2a/80/f46b8c681195190b2c1f1c7c0a81abce60663e987613e09ef64d433dd96b/pydantic_core-2.46.5-cp314-cp314-win32.whl", hash = "sha256:2d5d76654becf5efd62c9e51c3756c67b49498b0c9a40884934c40807adbd074", size = 1934827, upload-time = "2026-08-28T09:59:39.836Z" }, - { url = "https://files.pythonhosted.org/packages/f7/3c/60674207246bc0a4009d2391b7c7251c7159f279c8d2ab8aae8ef46f3dee/pydantic_core-2.46.5-cp314-cp314-win_amd64.whl", hash = "sha256:fa10ef4112775900e7a0661068635eb67b2ab824fbde764de6e0e21982a93db0", size = 2042648, upload-time = "2026-08-28T09:59:41.792Z" }, - { url = "https://files.pythonhosted.org/packages/69/0c/117c562c7c1babdf44576b72a5e496906506c93690387ecfbca7c729ae2e/pydantic_core-2.46.5-cp314-cp314-win_arm64.whl", hash = "sha256:045ab3b6d308439e32b81cc173bba5b9018bc6ed896afd0c65b3b009b1699af5", size = 1989652, upload-time = "2026-08-28T09:59:43.702Z" }, - { url = "https://files.pythonhosted.org/packages/e8/66/9336ae58f9eb68c41d121894e52c4c89eccb07eb8f602a04ee9c3f37736a/pydantic_core-2.46.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8816f3d218beb4b787de5c9759c259b8fa61f9dec42dc7811f320a33771778b7", size = 2065829, upload-time = "2026-08-28T09:59:45.364Z" }, - { url = "https://files.pythonhosted.org/packages/c5/02/bc19b47a96c2d3109760711acf22369e56bd7e405ca52f7ade164d2ead57/pydantic_core-2.46.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:bce57638e08ac148e5778cce7feb968307a727d66f8e2274a543d0cf0c9ad6a3", size = 1905716, upload-time = "2026-08-28T09:59:47.18Z" }, - { url = "https://files.pythonhosted.org/packages/52/a4/70b47c0509923dd98ccfed04fb3e32ea3849c82a0ff2205bb41009b43c00/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:976e1128455aa595ea04c79ccfedff1aaeab96ee013fcc916bed120c4f0ad94f", size = 1934216, upload-time = "2026-08-28T09:59:49.241Z" }, - { url = "https://files.pythonhosted.org/packages/52/ab/aa03b65f7bb198585edf806b906c3223ecf1795543e39e23aec4cce27ad2/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b891faeedeafba41b2983e5001a81b6a915b69544c7e7570d1989ce1c36ac7", size = 2010635, upload-time = "2026-08-28T09:59:51.692Z" }, - { url = "https://files.pythonhosted.org/packages/3c/8b/0da06343f30b84ec549aafd309c6456223d5dc8bd36af504c573faad561d/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5f194189415698233dd1114a093a9b56e61e2c57e11b469be3b0506f46f0771c", size = 2209369, upload-time = "2026-08-28T09:59:53.582Z" }, - { url = "https://files.pythonhosted.org/packages/d6/5b/844c4defaa34a3df66eb9257087d121d70c201298b96abdf9f492fc2f1bf/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:82a36973cf8a2ef5406f4fe2edbf8ed0c99629535d959e0b100c76a32535a111", size = 2253238, upload-time = "2026-08-28T09:59:55.484Z" }, - { url = "https://files.pythonhosted.org/packages/f4/64/a4e536cb16d7f61a7fd3120b46c577fc7fa7325992f69c4f52bc786d77d8/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdbb78909f52b981d3b2d56b97328d71eb0b974c36bd77c920123a7ebb192829", size = 2065740, upload-time = "2026-08-28T09:59:58.038Z" }, - { url = "https://files.pythonhosted.org/packages/5f/75/aaa38c6bc2d085f6605b34eabdc6a8a4e0b2e61fc9c8e6e52b28e97b3125/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:52e24eacdb536cade636aa90fb851835222becff8484b7001fdc78cb0290f2aa", size = 2087425, upload-time = "2026-08-28T09:59:59.898Z" }, - { url = "https://files.pythonhosted.org/packages/55/ae/fcab4cfc39aba3689e1d20c8b5250ad280957022c09af2ed9cd585602a5e/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:37ae34309d7bd8c0d61ab839668058f2a7962ea1fc51d105d2db228fe0618034", size = 2139306, upload-time = "2026-08-28T10:00:03.057Z" }, - { url = "https://files.pythonhosted.org/packages/2d/f4/f1d03a4bc9d9acbc62f4d742b8a319af52f71885079868b2ff8e48a651ee/pydantic_core-2.46.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:0cdbada856a1c69a7624a64d3d9aefe79300bd6ef827b43a4f265010b9b55184", size = 2144589, upload-time = "2026-08-28T10:00:05.645Z" }, - { url = "https://files.pythonhosted.org/packages/83/f3/7a53bb1356de514a4cd295f25b6ac39237895620c0462d2592b76c16e114/pydantic_core-2.46.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:545f26c504b27c3758439a5e6d9349931f0a04f855668d5fe323c89e82300a38", size = 2288882, upload-time = "2026-08-28T10:00:07.931Z" }, - { url = "https://files.pythonhosted.org/packages/cd/94/5a81583660c175c59d49ffb09f4b3a44debeaf86a19fca664ae1cdd9ee32/pydantic_core-2.46.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:ff218293c9c806138dca139765e3b067621be52bcd93cdc14c7711be7ddc90a9", size = 2335210, upload-time = "2026-08-28T10:00:10.177Z" }, - { url = "https://files.pythonhosted.org/packages/5a/9f/5d685c2693b972d1a59c998586e8823712b66603aeff47ee60a4bdaafd37/pydantic_core-2.46.5-cp314-cp314t-win32.whl", hash = "sha256:97cf3eb53a8cccacf9d46686a0926186c9bfb5574f2ed66d3639d5fe117cd3a9", size = 1921180, upload-time = "2026-08-28T10:00:12.35Z" }, - { url = "https://files.pythonhosted.org/packages/70/12/5c94ee16d65a37a15f9e869f5e6256df111154491173801a4c5e800ab548/pydantic_core-2.46.5-cp314-cp314t-win_amd64.whl", hash = "sha256:d2f9fc07a8042a8f95925b35c4f04f469707c981fc33245b6ca187cf5d2dd290", size = 2020515, upload-time = "2026-08-28T10:00:14.774Z" }, - { url = "https://files.pythonhosted.org/packages/63/19/67830dda664e6bdf9285ee2e40f355d0d7d6b92aa0c42e8d217bb8d33d36/pydantic_core-2.46.5-cp314-cp314t-win_arm64.whl", hash = "sha256:acf8a67ba51f4ca9ddbd0e6b3000a65ac51ab734661778b3e7ba64d99a710f2f", size = 1989276, upload-time = "2026-08-28T10:00:16.984Z" }, - { url = "https://files.pythonhosted.org/packages/af/1e/ecca01fce348f7e8afa9572441ff6f7d1cc70d21e4859f33944d10877e1e/pydantic_core-2.46.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:c14ad3bdc85ee7f318742c457ca3968a92126d144b15721c759033bfb06296c2", size = 2075342, upload-time = "2026-08-28T10:00:51.353Z" }, - { url = "https://files.pythonhosted.org/packages/1f/4c/af80c7a8032dfc897040ad5cb772bebde529a381186499e6e29987f23f8c/pydantic_core-2.46.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:0bddb4020d8f04175865ccd17eff3040874fc11fb593f424edb452653b4b947c", size = 1907219, upload-time = "2026-08-28T10:00:53.438Z" }, - { url = "https://files.pythonhosted.org/packages/be/3e/54d89e2b092e778716bf6153634ef479e955f48c261090be23aa1e0fb0b5/pydantic_core-2.46.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2471fd51c61c610e1dcf7de44d7299283661654d11264ab4802b303368d69c47", size = 1953393, upload-time = "2026-08-28T10:00:55.58Z" }, - { url = "https://files.pythonhosted.org/packages/ea/89/828ee90cda28ce17bdefaa3a6eaf74fe430e113295a10e6126beca559d6c/pydantic_core-2.46.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b10ec717381bdbfafef34607824db4c91de69ff085e4fca3b2af91b4fa17e68a", size = 2099024, upload-time = "2026-08-28T10:00:57.794Z" }, - { url = "https://files.pythonhosted.org/packages/df/dd/053c2e4303f791f3b8f8a14ab0b22008e8eb21d868c0c90b4f9be705b76a/pydantic_core-2.46.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:013d6f3483d81e02e7c328831808f336c8596ee33b4bd4026b9ffb1e960b8942", size = 2062540, upload-time = "2026-08-28T10:01:00.318Z" }, - { url = "https://files.pythonhosted.org/packages/d7/dd/a18df751a5e37dd51bfad7f68e766999125bebe68c9e1d10a493ad01bd63/pydantic_core-2.46.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:e9c134bb666dd54b778b9fc0d2b50cbb7f979b9e3716f26a88c9ab3b6fc1dd0f", size = 1902040, upload-time = "2026-08-28T10:01:02.529Z" }, - { url = "https://files.pythonhosted.org/packages/b7/13/01d40f9d07ce8a779fd6e0bd8ad4fba91309500dd67b869e2e219d261a6d/pydantic_core-2.46.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:347ec774390c87326a2e4929d58d3f7e8763a104d5d35f4cd595a4c952366433", size = 1967479, upload-time = "2026-08-28T10:01:05.004Z" }, - { url = "https://files.pythonhosted.org/packages/fa/04/c81d4841331c2178b6fb09ae225425e110ed72d990c9fe556c4ec03d1013/pydantic_core-2.46.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8e24d8f05fa2d28513d94e877e9c75ad66175376209b3977f916e240e623193c", size = 2111034, upload-time = "2026-08-28T10:01:07.345Z" }, - { url = "https://files.pythonhosted.org/packages/20/21/22102e9950b3049526d20e811b95396508377d87651edd2b80d2b3d28659/pydantic_core-2.46.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:ab4b66edffb32d9e951efb3814bd104b8367a7501b81b955cacb5726d897389f", size = 2071333, upload-time = "2026-08-28T10:01:09.636Z" }, - { url = "https://files.pythonhosted.org/packages/d8/18/87aefa427d191e6d3ab1447f1efc1cdcac86af1069239b133e8a0fd7f7c9/pydantic_core-2.46.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:337639ba62a11acde6ef3aeb08c8ea755f8ef1fe5e513356c0f36a2b0d7568b0", size = 1912713, upload-time = "2026-08-28T10:01:12.285Z" }, - { url = "https://files.pythonhosted.org/packages/1f/93/fd89e9ad49b1805ca94d24ce1088b7d305f05c35ffafcedb9819d03588a0/pydantic_core-2.46.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:413a717a410d0c817ef5b786a059415550b3794e1d0c2abffd9efb93a3d9f7b4", size = 2090926, upload-time = "2026-08-28T10:01:15.19Z" }, - { url = "https://files.pythonhosted.org/packages/6f/45/8e59dab6acf8d35f02f0a958980074f31038968bdb2c983fcae9d1efee03/pydantic_core-2.46.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1e449def1945a462c464331254e5a44fca7c3b4f9aedf59ec2f50f8066dd8e25", size = 2131303, upload-time = "2026-08-28T10:01:17.937Z" }, - { url = "https://files.pythonhosted.org/packages/d5/a5/e1d4dc5180dd887a9522efc1f8716b8692b7606b1d3273d7862eaf66be44/pydantic_core-2.46.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:a445486499897b88a7d6c310c88ed64dd37b1b59bfd7ae9107490bbb362f47d6", size = 2145128, upload-time = "2026-08-28T10:01:20.694Z" }, - { url = "https://files.pythonhosted.org/packages/c2/d7/ad493864a7fb21c0c4df98f965e2db430cb25a9d7369b5778d5016c09fd9/pydantic_core-2.46.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:2d330aaba8621b1edcec8ae2c4050f63b84ccf6d98723a8f212e9684713abf0e", size = 2294560, upload-time = "2026-08-28T10:01:23.495Z" }, - { url = "https://files.pythonhosted.org/packages/02/8e/b41c84c913f29973a268e6c2b5bbf13c95adb9956c126d10da11ba3b2bef/pydantic_core-2.46.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:b6acfb46a814762367fb7ba0828b0a17d441b92ce249a0e007474c9072662dda", size = 2317531, upload-time = "2026-08-28T10:01:26.334Z" }, - { url = "https://files.pythonhosted.org/packages/db/1d/068464f23075f66a8f1b806935e9cd9363ee446636ea70d2c22ee8659dbf/pydantic_core-2.46.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:d0a24b40877af2de4950252be9d21eaf7fb07660f3c2cae1f56c6b599ada5266", size = 2140686, upload-time = "2026-08-28T10:01:28.947Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/72/74a989dd9f2084b3d9530b0915fdda64ac48831c30dbf7c72a41a5232db8/pydantic_core-2.41.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a3a52f6156e73e7ccb0f8cced536adccb7042be67cb45f9562e12b319c119da6", size = 2105873, upload-time = "2025-11-04T13:39:31.373Z" }, + { url = "https://files.pythonhosted.org/packages/12/44/37e403fd9455708b3b942949e1d7febc02167662bf1a7da5b78ee1ea2842/pydantic_core-2.41.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7f3bf998340c6d4b0c9a2f02d6a400e51f123b59565d74dc60d252ce888c260b", size = 1899826, upload-time = "2025-11-04T13:39:32.897Z" }, + { url = "https://files.pythonhosted.org/packages/33/7f/1d5cab3ccf44c1935a359d51a8a2a9e1a654b744b5e7f80d41b88d501eec/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:378bec5c66998815d224c9ca994f1e14c0c21cb95d2f52b6021cc0b2a58f2a5a", size = 1917869, upload-time = "2025-11-04T13:39:34.469Z" }, + { url = "https://files.pythonhosted.org/packages/6e/6a/30d94a9674a7fe4f4744052ed6c5e083424510be1e93da5bc47569d11810/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b576130c69225432866fe2f4a469a85a54ade141d96fd396dffcf607b558f8", size = 2063890, upload-time = "2025-11-04T13:39:36.053Z" }, + { url = "https://files.pythonhosted.org/packages/50/be/76e5d46203fcb2750e542f32e6c371ffa9b8ad17364cf94bb0818dbfb50c/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6cb58b9c66f7e4179a2d5e0f849c48eff5c1fca560994d6eb6543abf955a149e", size = 2229740, upload-time = "2025-11-04T13:39:37.753Z" }, + { url = "https://files.pythonhosted.org/packages/d3/ee/fed784df0144793489f87db310a6bbf8118d7b630ed07aa180d6067e653a/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88942d3a3dff3afc8288c21e565e476fc278902ae4d6d134f1eeda118cc830b1", size = 2350021, upload-time = "2025-11-04T13:39:40.94Z" }, + { url = "https://files.pythonhosted.org/packages/c8/be/8fed28dd0a180dca19e72c233cbf58efa36df055e5b9d90d64fd1740b828/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f31d95a179f8d64d90f6831d71fa93290893a33148d890ba15de25642c5d075b", size = 2066378, upload-time = "2025-11-04T13:39:42.523Z" }, + { url = "https://files.pythonhosted.org/packages/b0/3b/698cf8ae1d536a010e05121b4958b1257f0b5522085e335360e53a6b1c8b/pydantic_core-2.41.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c1df3d34aced70add6f867a8cf413e299177e0c22660cc767218373d0779487b", size = 2175761, upload-time = "2025-11-04T13:39:44.553Z" }, + { url = "https://files.pythonhosted.org/packages/b8/ba/15d537423939553116dea94ce02f9c31be0fa9d0b806d427e0308ec17145/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4009935984bd36bd2c774e13f9a09563ce8de4abaa7226f5108262fa3e637284", size = 2146303, upload-time = "2025-11-04T13:39:46.238Z" }, + { url = "https://files.pythonhosted.org/packages/58/7f/0de669bf37d206723795f9c90c82966726a2ab06c336deba4735b55af431/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:34a64bc3441dc1213096a20fe27e8e128bd3ff89921706e83c0b1ac971276594", size = 2340355, upload-time = "2025-11-04T13:39:48.002Z" }, + { url = "https://files.pythonhosted.org/packages/e5/de/e7482c435b83d7e3c3ee5ee4451f6e8973cff0eb6007d2872ce6383f6398/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c9e19dd6e28fdcaa5a1de679aec4141f691023916427ef9bae8584f9c2fb3b0e", size = 2319875, upload-time = "2025-11-04T13:39:49.705Z" }, + { url = "https://files.pythonhosted.org/packages/fe/e6/8c9e81bb6dd7560e33b9053351c29f30c8194b72f2d6932888581f503482/pydantic_core-2.41.5-cp311-cp311-win32.whl", hash = "sha256:2c010c6ded393148374c0f6f0bf89d206bf3217f201faa0635dcd56bd1520f6b", size = 1987549, upload-time = "2025-11-04T13:39:51.842Z" }, + { url = "https://files.pythonhosted.org/packages/11/66/f14d1d978ea94d1bc21fc98fcf570f9542fe55bfcc40269d4e1a21c19bf7/pydantic_core-2.41.5-cp311-cp311-win_amd64.whl", hash = "sha256:76ee27c6e9c7f16f47db7a94157112a2f3a00e958bc626e2f4ee8bec5c328fbe", size = 2011305, upload-time = "2025-11-04T13:39:53.485Z" }, + { url = "https://files.pythonhosted.org/packages/56/d8/0e271434e8efd03186c5386671328154ee349ff0354d83c74f5caaf096ed/pydantic_core-2.41.5-cp311-cp311-win_arm64.whl", hash = "sha256:4bc36bbc0b7584de96561184ad7f012478987882ebf9f9c389b23f432ea3d90f", size = 1972902, upload-time = "2025-11-04T13:39:56.488Z" }, + { url = "https://files.pythonhosted.org/packages/5f/5d/5f6c63eebb5afee93bcaae4ce9a898f3373ca23df3ccaef086d0233a35a7/pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7", size = 2110990, upload-time = "2025-11-04T13:39:58.079Z" }, + { url = "https://files.pythonhosted.org/packages/aa/32/9c2e8ccb57c01111e0fd091f236c7b371c1bccea0fa85247ac55b1e2b6b6/pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0", size = 1896003, upload-time = "2025-11-04T13:39:59.956Z" }, + { url = "https://files.pythonhosted.org/packages/68/b8/a01b53cb0e59139fbc9e4fda3e9724ede8de279097179be4ff31f1abb65a/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69", size = 1919200, upload-time = "2025-11-04T13:40:02.241Z" }, + { url = "https://files.pythonhosted.org/packages/38/de/8c36b5198a29bdaade07b5985e80a233a5ac27137846f3bc2d3b40a47360/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75", size = 2052578, upload-time = "2025-11-04T13:40:04.401Z" }, + { url = "https://files.pythonhosted.org/packages/00/b5/0e8e4b5b081eac6cb3dbb7e60a65907549a1ce035a724368c330112adfdd/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05", size = 2208504, upload-time = "2025-11-04T13:40:06.072Z" }, + { url = "https://files.pythonhosted.org/packages/77/56/87a61aad59c7c5b9dc8caad5a41a5545cba3810c3e828708b3d7404f6cef/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc", size = 2335816, upload-time = "2025-11-04T13:40:07.835Z" }, + { url = "https://files.pythonhosted.org/packages/0d/76/941cc9f73529988688a665a5c0ecff1112b3d95ab48f81db5f7606f522d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c", size = 2075366, upload-time = "2025-11-04T13:40:09.804Z" }, + { url = "https://files.pythonhosted.org/packages/d3/43/ebef01f69baa07a482844faaa0a591bad1ef129253ffd0cdaa9d8a7f72d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5", size = 2171698, upload-time = "2025-11-04T13:40:12.004Z" }, + { url = "https://files.pythonhosted.org/packages/b1/87/41f3202e4193e3bacfc2c065fab7706ebe81af46a83d3e27605029c1f5a6/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c", size = 2132603, upload-time = "2025-11-04T13:40:13.868Z" }, + { url = "https://files.pythonhosted.org/packages/49/7d/4c00df99cb12070b6bccdef4a195255e6020a550d572768d92cc54dba91a/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294", size = 2329591, upload-time = "2025-11-04T13:40:15.672Z" }, + { url = "https://files.pythonhosted.org/packages/cc/6a/ebf4b1d65d458f3cda6a7335d141305dfa19bdc61140a884d165a8a1bbc7/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1", size = 2319068, upload-time = "2025-11-04T13:40:17.532Z" }, + { url = "https://files.pythonhosted.org/packages/49/3b/774f2b5cd4192d5ab75870ce4381fd89cf218af999515baf07e7206753f0/pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d", size = 1985908, upload-time = "2025-11-04T13:40:19.309Z" }, + { url = "https://files.pythonhosted.org/packages/86/45/00173a033c801cacf67c190fef088789394feaf88a98a7035b0e40d53dc9/pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815", size = 2020145, upload-time = "2025-11-04T13:40:21.548Z" }, + { url = "https://files.pythonhosted.org/packages/f9/22/91fbc821fa6d261b376a3f73809f907cec5ca6025642c463d3488aad22fb/pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3", size = 1976179, upload-time = "2025-11-04T13:40:23.393Z" }, + { url = "https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", size = 2120403, upload-time = "2025-11-04T13:40:25.248Z" }, + { url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206, upload-time = "2025-11-04T13:40:27.099Z" }, + { url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307, upload-time = "2025-11-04T13:40:29.806Z" }, + { url = "https://files.pythonhosted.org/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", size = 2063258, upload-time = "2025-11-04T13:40:33.544Z" }, + { url = "https://files.pythonhosted.org/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", size = 2214917, upload-time = "2025-11-04T13:40:35.479Z" }, + { url = "https://files.pythonhosted.org/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", size = 2332186, upload-time = "2025-11-04T13:40:37.436Z" }, + { url = "https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", size = 2073164, upload-time = "2025-11-04T13:40:40.289Z" }, + { url = "https://files.pythonhosted.org/packages/bf/e3/f6e262673c6140dd3305d144d032f7bd5f7497d3871c1428521f19f9efa2/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", size = 2179146, upload-time = "2025-11-04T13:40:42.809Z" }, + { url = "https://files.pythonhosted.org/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", size = 2137788, upload-time = "2025-11-04T13:40:44.752Z" }, + { url = "https://files.pythonhosted.org/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", size = 2340133, upload-time = "2025-11-04T13:40:46.66Z" }, + { url = "https://files.pythonhosted.org/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", size = 2324852, upload-time = "2025-11-04T13:40:48.575Z" }, + { url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679, upload-time = "2025-11-04T13:40:50.619Z" }, + { url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766, upload-time = "2025-11-04T13:40:52.631Z" }, + { url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005, upload-time = "2025-11-04T13:40:54.734Z" }, + { url = "https://files.pythonhosted.org/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", size = 2119622, upload-time = "2025-11-04T13:40:56.68Z" }, + { url = "https://files.pythonhosted.org/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", size = 1891725, upload-time = "2025-11-04T13:40:58.807Z" }, + { url = "https://files.pythonhosted.org/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", size = 1915040, upload-time = "2025-11-04T13:41:00.853Z" }, + { url = "https://files.pythonhosted.org/packages/84/a3/15a82ac7bd97992a82257f777b3583d3e84bdb06ba6858f745daa2ec8a85/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", size = 2063691, upload-time = "2025-11-04T13:41:03.504Z" }, + { url = "https://files.pythonhosted.org/packages/74/9b/0046701313c6ef08c0c1cf0e028c67c770a4e1275ca73131563c5f2a310a/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", size = 2213897, upload-time = "2025-11-04T13:41:05.804Z" }, + { url = "https://files.pythonhosted.org/packages/8a/cd/6bac76ecd1b27e75a95ca3a9a559c643b3afcd2dd62086d4b7a32a18b169/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", size = 2333302, upload-time = "2025-11-04T13:41:07.809Z" }, + { url = "https://files.pythonhosted.org/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", size = 2064877, upload-time = "2025-11-04T13:41:09.827Z" }, + { url = "https://files.pythonhosted.org/packages/18/66/e9db17a9a763d72f03de903883c057b2592c09509ccfe468187f2a2eef29/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", size = 2180680, upload-time = "2025-11-04T13:41:12.379Z" }, + { url = "https://files.pythonhosted.org/packages/d3/9e/3ce66cebb929f3ced22be85d4c2399b8e85b622db77dad36b73c5387f8f8/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", size = 2138960, upload-time = "2025-11-04T13:41:14.627Z" }, + { url = "https://files.pythonhosted.org/packages/a6/62/205a998f4327d2079326b01abee48e502ea739d174f0a89295c481a2272e/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", size = 2339102, upload-time = "2025-11-04T13:41:16.868Z" }, + { url = "https://files.pythonhosted.org/packages/3c/0d/f05e79471e889d74d3d88f5bd20d0ed189ad94c2423d81ff8d0000aab4ff/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", size = 2326039, upload-time = "2025-11-04T13:41:18.934Z" }, + { url = "https://files.pythonhosted.org/packages/ec/e1/e08a6208bb100da7e0c4b288eed624a703f4d129bde2da475721a80cab32/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", size = 1995126, upload-time = "2025-11-04T13:41:21.418Z" }, + { url = "https://files.pythonhosted.org/packages/48/5d/56ba7b24e9557f99c9237e29f5c09913c81eeb2f3217e40e922353668092/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", size = 2015489, upload-time = "2025-11-04T13:41:24.076Z" }, + { url = "https://files.pythonhosted.org/packages/4e/bb/f7a190991ec9e3e0ba22e4993d8755bbc4a32925c0b5b42775c03e8148f9/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", size = 1977288, upload-time = "2025-11-04T13:41:26.33Z" }, + { url = "https://files.pythonhosted.org/packages/92/ed/77542d0c51538e32e15afe7899d79efce4b81eee631d99850edc2f5e9349/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", size = 2120255, upload-time = "2025-11-04T13:41:28.569Z" }, + { url = "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", size = 1863760, upload-time = "2025-11-04T13:41:31.055Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f0/e5e6b99d4191da102f2b0eb9687aaa7f5bea5d9964071a84effc3e40f997/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", size = 1878092, upload-time = "2025-11-04T13:41:33.21Z" }, + { url = "https://files.pythonhosted.org/packages/71/48/36fb760642d568925953bcc8116455513d6e34c4beaa37544118c36aba6d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", size = 2053385, upload-time = "2025-11-04T13:41:35.508Z" }, + { url = "https://files.pythonhosted.org/packages/20/25/92dc684dd8eb75a234bc1c764b4210cf2646479d54b47bf46061657292a8/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", size = 2218832, upload-time = "2025-11-04T13:41:37.732Z" }, + { url = "https://files.pythonhosted.org/packages/e2/09/f53e0b05023d3e30357d82eb35835d0f6340ca344720a4599cd663dca599/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", size = 2327585, upload-time = "2025-11-04T13:41:40Z" }, + { url = "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", size = 2041078, upload-time = "2025-11-04T13:41:42.323Z" }, + { url = "https://files.pythonhosted.org/packages/cd/13/2e215f17f0ef326fc72afe94776edb77525142c693767fc347ed6288728d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", size = 2173914, upload-time = "2025-11-04T13:41:45.221Z" }, + { url = "https://files.pythonhosted.org/packages/02/7a/f999a6dcbcd0e5660bc348a3991c8915ce6599f4f2c6ac22f01d7a10816c/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", size = 2129560, upload-time = "2025-11-04T13:41:47.474Z" }, + { url = "https://files.pythonhosted.org/packages/3a/b1/6c990ac65e3b4c079a4fb9f5b05f5b013afa0f4ed6780a3dd236d2cbdc64/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", size = 2329244, upload-time = "2025-11-04T13:41:49.992Z" }, + { url = "https://files.pythonhosted.org/packages/d9/02/3c562f3a51afd4d88fff8dffb1771b30cfdfd79befd9883ee094f5b6c0d8/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", size = 2331955, upload-time = "2025-11-04T13:41:54.079Z" }, + { url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906, upload-time = "2025-11-04T13:41:56.606Z" }, + { url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607, upload-time = "2025-11-04T13:41:58.889Z" }, + { url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769, upload-time = "2025-11-04T13:42:01.186Z" }, + { url = "https://files.pythonhosted.org/packages/11/72/90fda5ee3b97e51c494938a4a44c3a35a9c96c19bba12372fb9c634d6f57/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b96d5f26b05d03cc60f11a7761a5ded1741da411e7fe0909e27a5e6a0cb7b034", size = 2115441, upload-time = "2025-11-04T13:42:39.557Z" }, + { url = "https://files.pythonhosted.org/packages/1f/53/8942f884fa33f50794f119012dc6a1a02ac43a56407adaac20463df8e98f/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c", size = 1930291, upload-time = "2025-11-04T13:42:42.169Z" }, + { url = "https://files.pythonhosted.org/packages/79/c8/ecb9ed9cd942bce09fc888ee960b52654fbdbede4ba6c2d6e0d3b1d8b49c/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2", size = 1948632, upload-time = "2025-11-04T13:42:44.564Z" }, + { url = "https://files.pythonhosted.org/packages/2e/1b/687711069de7efa6af934e74f601e2a4307365e8fdc404703afc453eab26/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f15489ba13d61f670dcc96772e733aad1a6f9c429cc27574c6cdaed82d0146ad", size = 2138905, upload-time = "2025-11-04T13:42:47.156Z" }, + { url = "https://files.pythonhosted.org/packages/09/32/59b0c7e63e277fa7911c2fc70ccfb45ce4b98991e7ef37110663437005af/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd", size = 2110495, upload-time = "2025-11-04T13:42:49.689Z" }, + { url = "https://files.pythonhosted.org/packages/aa/81/05e400037eaf55ad400bcd318c05bb345b57e708887f07ddb2d20e3f0e98/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc", size = 1915388, upload-time = "2025-11-04T13:42:52.215Z" }, + { url = "https://files.pythonhosted.org/packages/6e/0d/e3549b2399f71d56476b77dbf3cf8937cec5cd70536bdc0e374a421d0599/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56", size = 1942879, upload-time = "2025-11-04T13:42:56.483Z" }, + { url = "https://files.pythonhosted.org/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", size = 2139017, upload-time = "2025-11-04T13:42:59.471Z" }, + { url = "https://files.pythonhosted.org/packages/5f/9b/1b3f0e9f9305839d7e84912f9e8bfbd191ed1b1ef48083609f0dabde978c/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b2379fa7ed44ddecb5bfe4e48577d752db9fc10be00a6b7446e9663ba143de26", size = 2101980, upload-time = "2025-11-04T13:43:25.97Z" }, + { url = "https://files.pythonhosted.org/packages/a4/ed/d71fefcb4263df0da6a85b5d8a7508360f2f2e9b3bf5814be9c8bccdccc1/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808", size = 1923865, upload-time = "2025-11-04T13:43:28.763Z" }, + { url = "https://files.pythonhosted.org/packages/ce/3a/626b38db460d675f873e4444b4bb030453bbe7b4ba55df821d026a0493c4/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc", size = 2134256, upload-time = "2025-11-04T13:43:31.71Z" }, + { url = "https://files.pythonhosted.org/packages/83/d9/8412d7f06f616bbc053d30cb4e5f76786af3221462ad5eee1f202021eb4e/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:287dad91cfb551c363dc62899a80e9e14da1f0e2b6ebde82c806612ca2a13ef1", size = 2174762, upload-time = "2025-11-04T13:43:34.744Z" }, + { url = "https://files.pythonhosted.org/packages/55/4c/162d906b8e3ba3a99354e20faa1b49a85206c47de97a639510a0e673f5da/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:03b77d184b9eb40240ae9fd676ca364ce1085f203e1b1256f8ab9984dca80a84", size = 2143141, upload-time = "2025-11-04T13:43:37.701Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f2/f11dd73284122713f5f89fc940f370d035fa8e1e078d446b3313955157fe/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:a668ce24de96165bb239160b3d854943128f4334822900534f2fe947930e5770", size = 2330317, upload-time = "2025-11-04T13:43:40.406Z" }, + { url = "https://files.pythonhosted.org/packages/88/9d/b06ca6acfe4abb296110fb1273a4d848a0bfb2ff65f3ee92127b3244e16b/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f14f8f046c14563f8eb3f45f499cc658ab8d10072961e07225e507adb700e93f", size = 2316992, upload-time = "2025-11-04T13:43:43.602Z" }, + { url = "https://files.pythonhosted.org/packages/36/c7/cfc8e811f061c841d7990b0201912c3556bfeb99cdcb7ed24adc8d6f8704/pydantic_core-2.41.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51", size = 2145302, upload-time = "2025-11-04T13:43:46.64Z" }, +] + +[[package]] +name = "pydantic-settings" +version = "2.13.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "python-dotenv" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/52/6d/fffca34caecc4a3f97bda81b2098da5e8ab7efc9a66e819074a11955d87e/pydantic_settings-2.13.1.tar.gz", hash = "sha256:b4c11847b15237fb0171e1462bf540e294affb9b86db4d9aa5c01730bdbe4025", size = 223826, upload-time = "2026-02-19T13:45:08.055Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/00/4b/ccc026168948fec4f7555b9164c724cf4125eac006e176541483d2c959be/pydantic_settings-2.13.1-py3-none-any.whl", hash = "sha256:d56fd801823dbeae7f0975e1f8c8e25c258eb75d278ea7abb5d9cebb01b56237", size = 58929, upload-time = "2026-02-19T13:45:06.034Z" }, ] [[package]] @@ -200,6 +1849,28 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/71/46/17f022dd3e953bf20a04a028a21ec746d942f8d2af30fa0f124fa0e6a684/pygments-2.21.0-py3-none-any.whl", hash = "sha256:2363c69b61c4a97c838da3b130dcd6468f4848992b21a82f2a63ec34377137d9", size = 1250147, upload-time = "2026-08-17T08:02:44.912Z" }, ] +[[package]] +name = "pyjwt" +version = "2.14.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/af/c3/8a3b59c25070cc61dc517fbdfa5dc0904670c96f605cc69759dc09166b99/pyjwt-2.14.0.tar.gz", hash = "sha256:77283c83fb56ecf566a886c757a714bc83668e38156de2cce8263302f42e0b86", size = 113177, upload-time = "2026-09-11T13:11:54.638Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9c/97/672cb32ce0dfea44b740cb7b4f97038463b9cf7c0ead1aacf595572851d6/pyjwt-2.14.0-py3-none-any.whl", hash = "sha256:ad0cef71c756a56e74863c2919cf0985f72decbcfcb550ee2f422e7c62b5eedc", size = 32896, upload-time = "2026-09-11T13:11:53.409Z" }, +] + +[[package]] +name = "pyopenssl" +version = "26.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3f/e8/7325d258199b159eb2c03fe32107533e2832e70e63f4fb88a6aa00023201/pyopenssl-26.4.0.tar.gz", hash = "sha256:28dfcce0162b9211413e26dfbfdf1d24317fbeba18fc93c12400a1856b2a0bc7", size = 182046, upload-time = "2026-08-01T19:50:50.512Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/ad/2cf6d3fa2fae5c79e1ed9960c0d42badd0f94d81dd12b50604cdc839e648/pyopenssl-26.4.0-py3-none-any.whl", hash = "sha256:f0eb0cb2d581d3ad2b9c489468485e7f2ab6727d08401bcf9d824c3caddf3c1c", size = 56026, upload-time = "2026-08-01T19:50:48.94Z" }, +] + [[package]] name = "pytest" version = "9.0.3" @@ -228,6 +1899,45 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/cb/29/518f32faf6edad9f56d6e0107217f7de6b79f297a47170414a2bd4be7f01/pytest_snapshot-0.9.0-py3-none-any.whl", hash = "sha256:4b9fe1c21c868fe53a545e4e3184d36bc1c88946e3f5c1d9dd676962a9b3d4ab", size = 10715, upload-time = "2022-04-23T17:35:30.288Z" }, ] +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, +] + +[[package]] +name = "python-dotenv" +version = "1.2.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6a/53/ed9d74092561d4b01a2ef1349d52cdbc135e526c245f366b089cfca6de49/python_dotenv-1.2.3.tar.gz", hash = "sha256:a20a594dabeaa385725aa239d5244871c143ecb356add8a20fcf23773a6c3a35", size = 58945, upload-time = "2026-08-16T16:54:54.067Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0d/17/c5c6b53ddc18f297992099b3d9ec16c855c0ccc83263a21fe4d1c625ec6c/python_dotenv-1.2.3-py3-none-any.whl", hash = "sha256:904552145e8bfed22162c09dab1c2b9b54fefa7b23ba780f4f26ca0316b0f0d9", size = 22780, upload-time = "2026-08-16T16:54:52.473Z" }, +] + +[[package]] +name = "pytz" +version = "2026.3.post1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fb/48/fb042503b6ca6cd271261dc559fd6432f7d8c713153e9ec5c591af4dfc1c/pytz-2026.3.post1.tar.gz", hash = "sha256:2211d3fcf9a797d3405cac96ac7f61d80e6a644f72a3309607282fe8a2010c5d", size = 319745, upload-time = "2026-07-25T15:12:07.385Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0f/7b/39c34ca613b0b198cb866466651b26b045e2009864c5183c979a3b83f383/pytz-2026.3.post1-py2.py3-none-any.whl", hash = "sha256:dd95840dd199baea12d9cc096a1d452caa6596a1c1e4b5f3dbd1541855d5e815", size = 508283, upload-time = "2026-07-25T15:12:05.782Z" }, +] + +[[package]] +name = "pywin32-ctypes" +version = "0.2.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/85/9f/01a1a99704853cb63f253eea009390c88e7131c67e66a0a02099a8c917cb/pywin32-ctypes-0.2.3.tar.gz", hash = "sha256:d162dc04946d704503b2edc4d55f3dba5c1d539ead017afa00142c38b9885755", size = 29471, upload-time = "2024-08-14T10:15:34.626Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/3d/8161f7711c017e01ac9f008dfddd9410dff3674334c233bde66e7ba65bbf/pywin32_ctypes-0.2.3-py3-none-any.whl", hash = "sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8", size = 30756, upload-time = "2024-08-14T10:15:33.187Z" }, +] + [[package]] name = "pyyaml" version = "6.0.3" @@ -283,6 +1993,245 @@ wheels = [ { 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 = "relationalai" +version = "1.27.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohttp" }, + { name = "click" }, + { name = "confocal" }, + { name = "cryptography" }, + { name = "duckdb" }, + { name = "fastapi" }, + { name = "gravis" }, + { name = "httpx" }, + { name = "inquirerpy" }, + { name = "lqp" }, + { name = "more-itertools" }, + { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, + { name = "numpy", version = "2.5.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "opentelemetry-api" }, + { name = "opentelemetry-sdk" }, + { name = "packaging" }, + { name = "pandas" }, + { name = "pyarrow" }, + { name = "pydantic" }, + { name = "pydantic-settings" }, + { name = "pyjwt" }, + { name = "pyyaml" }, + { name = "requests" }, + { name = "rich" }, + { name = "snowflake-connector-python", extra = ["secure-local-storage"] }, + { name = "snowflake-snowpark-python" }, + { name = "sqlglot" }, + { name = "toml" }, + { name = "typing-extensions" }, + { name = "uvicorn", extra = ["standard"] }, + { name = "websockets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8e/52/e56a51c34eb5b52ea61aec634d5dbcadfe89f9f2e2d68f646087b3c7d71d/relationalai-1.27.1.tar.gz", hash = "sha256:7858e84c0dac5c4a110cce32276676e34cf54275c9074dd2d12f2e0ef81a018d", size = 3350290, upload-time = "2026-08-18T20:59:31.369Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/66/5a8fda57c9ae592ffbac648d944f08babee6e7af4208b36de02560363586/relationalai-1.27.1-py3-none-any.whl", hash = "sha256:650171cdc31e2224f8e3b3555b415ee4ccea958b432875aad76c712c002e5ebf", size = 3865831, upload-time = "2026-08-18T20:59:29.71Z" }, +] + +[[package]] +name = "requests" +version = "2.34.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ac/c3/e2a2b89f2d3e2179abd6d00ebd70bff6273f37fb3e0cc209f48b39d00cbf/requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed", size = 142856, upload-time = "2026-05-14T19:25:27.735Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", size = 73075, upload-time = "2026-05-14T19:25:26.443Z" }, +] + +[[package]] +name = "rich" +version = "15.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36", size = 230680, upload-time = "2026-04-12T08:24:00.75Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654, upload-time = "2026-04-12T08:24:02.83Z" }, +] + +[[package]] +name = "s3transfer" +version = "0.19.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "botocore" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/76/43/35e4d8aa320bffe8287fe8f65f578fa2d2db0a64212f0e710dce58267854/s3transfer-0.19.2.tar.gz", hash = "sha256:ba0309fd86be3c27dbf78cdd813c13c5e1df16e5874b99d2535ebbdfb9892993", size = 165592, upload-time = "2026-07-22T19:30:44.432Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bc/e7/5c595c75e9f41a44f30e526eda465ea0b4eec93470e074e4a111b253f13a/s3transfer-0.19.2-py3-none-any.whl", hash = "sha256:d8168eccca828cbb2cd573675333f3bddd254313a9c42494b84c76b539e8ba25", size = 90216, upload-time = "2026-07-22T19:30:43.251Z" }, +] + +[[package]] +name = "secretstorage" +version = "3.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, + { name = "jeepney" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1c/03/e834bcd866f2f8a49a85eaff47340affa3bfa391ee9912a952a1faa68c7b/secretstorage-3.5.0.tar.gz", hash = "sha256:f04b8e4689cbce351744d5537bf6b1329c6fc68f91fa666f60a380edddcd11be", size = 19884, upload-time = "2025-11-23T19:02:53.191Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/46/f5af3402b579fd5e11573ce652019a67074317e18c1935cc0b4ba9b35552/secretstorage-3.5.0-py3-none-any.whl", hash = "sha256:0ce65888c0725fcb2c5bc0fdb8e5438eece02c523557ea40ce0703c266248137", size = 15554, upload-time = "2025-11-23T19:02:51.545Z" }, +] + +[[package]] +name = "setuptools" +version = "84.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6d/44/f5da03a8ef95d369145c5bb53050e7877c9f3d312e128605fd9504829143/setuptools-84.0.0.tar.gz", hash = "sha256:f4695c21257f0d9b537ec2692c941d02ee143b7cc1276941349a546573b2ef73", size = 1168449, upload-time = "2026-08-08T18:27:58.365Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/95/9c/c510029fc6ef33a6275cd2c5d3cecd6613dfd6aa401d57c54f1c18852ccf/setuptools-84.0.0-py3-none-any.whl", hash = "sha256:51a52592b3b99e102b609654876bd65f19f999935166d1352678931132b0c670", size = 818216, upload-time = "2026-08-08T18:27:56.719Z" }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + +[[package]] +name = "snowflake-connector-python" +version = "4.7.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "asn1crypto" }, + { name = "boto3" }, + { name = "botocore" }, + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "cryptography" }, + { name = "filelock" }, + { name = "idna" }, + { name = "packaging" }, + { name = "platformdirs" }, + { name = "pyjwt" }, + { name = "pyopenssl" }, + { name = "pytz" }, + { name = "requests" }, + { name = "sortedcontainers" }, + { name = "tomlkit" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/00/2c/bb5ecbd0af9f24b743de2d4a44497be2bdab0e4afcdcd23d96998a2f8d18/snowflake_connector_python-4.7.3.tar.gz", hash = "sha256:0f93139c3502c3d23139561b123492c09c33ab12a30956649a01e2cb04f06583", size = 968493, upload-time = "2026-09-03T14:13:37.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2d/36/a15f98e5fddcdd4ed4d4571035eaffcde5f25f7920a22a24fa5873835148/snowflake_connector_python-4.7.3-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:ac139500a41548d7be2c4756da8e4dc45a43581886e4189cf4870bff25dc526a", size = 1197289, upload-time = "2026-09-03T14:13:42.629Z" }, + { url = "https://files.pythonhosted.org/packages/49/e7/25c1e9f094124e284e0a529607846a43a9bbdf8a016e67e91e084ab2455b/snowflake_connector_python-4.7.3-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:88929f0f9724c9f160d0936cf02f95a0631991a9d660b74e7d38673205ec1fcb", size = 1209701, upload-time = "2026-09-03T14:13:44.232Z" }, + { url = "https://files.pythonhosted.org/packages/1b/c2/a71c96a1f11643b3e6f85703bc93697bbdf9a957fb7a1d1af9b055d64c13/snowflake_connector_python-4.7.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0e73865b67e11cd9a35e55d37a548df2b2d827e8e3abaed6b9ed49b886c14f17", size = 2891992, upload-time = "2026-09-03T14:13:21.433Z" }, + { url = "https://files.pythonhosted.org/packages/cb/7f/a46fc8057d2a9f483667b42ef4a5df8d2e8e92184c320a854e6cf7ab9bbc/snowflake_connector_python-4.7.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:380e286ebe5923d14b875c3e9e3b6a4cae695f0b3c37313384ef65dc56328110", size = 2920190, upload-time = "2026-09-03T14:13:22.852Z" }, + { url = "https://files.pythonhosted.org/packages/15/68/31224af3d2ac988347a92801f55e78de9a8848a12ed3b773b5bf1074ffaf/snowflake_connector_python-4.7.3-cp311-cp311-win_amd64.whl", hash = "sha256:635c1130cec0a057b924d1730948f0ec5992e868ac2c8ffa25f1e7a22fd12ded", size = 5433542, upload-time = "2026-09-03T14:13:59.357Z" }, + { url = "https://files.pythonhosted.org/packages/32/6f/bdc5436dcce960a4e0e300af0c2219aa0e83d566160b0fa9c391d2b5dab4/snowflake_connector_python-4.7.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:7d17f2a0e4b49156a57072d079a84db8471cbf511d52c88233df7e374936dbd5", size = 1196264, upload-time = "2026-09-03T14:13:45.555Z" }, + { url = "https://files.pythonhosted.org/packages/db/02/4957a4ebcde5ff2a8484e3ea1f26049b7e1838d7310564b1e603716f1a84/snowflake_connector_python-4.7.3-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:e18a1c12bd8c271c0aba043f0cf85d8466201ce1eda6a7aafa9fd6dfd4d5e7dc", size = 1209883, upload-time = "2026-09-03T14:13:46.847Z" }, + { url = "https://files.pythonhosted.org/packages/00/07/40fc43f4fe0e26a71597968cc3421e6858f499a4bfe5566ae712324a109e/snowflake_connector_python-4.7.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:af5b2cc588f10b4d12d21bd6232f61e78055bad2aeb58ce3b88ce71e4af24b51", size = 2932383, upload-time = "2026-09-03T14:13:24.312Z" }, + { url = "https://files.pythonhosted.org/packages/0d/b3/5ee12f4d6a74d2cb358d80a309eb1c6fe6672680f718de286637ce93d95d/snowflake_connector_python-4.7.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f11781d159f087d5ccf04d3c622a2eeb341b5540a15495cc072b0421b0d38fe1", size = 2965976, upload-time = "2026-09-03T14:13:25.662Z" }, + { url = "https://files.pythonhosted.org/packages/5c/b1/03cbe5c8601f99d51bffb78c51327c85e68b2273fd3b0aede2845cb691f9/snowflake_connector_python-4.7.3-cp312-cp312-win_amd64.whl", hash = "sha256:0fe0d7c9ff0d7cab163e7f470e119ce1ac67a264a4d644c8adba186a3e5b5a29", size = 5432021, upload-time = "2026-09-03T14:14:01.056Z" }, + { url = "https://files.pythonhosted.org/packages/ab/38/3b206805b46defb638d02156746d21ba559937e93ca0f04a143dcd0aeceb/snowflake_connector_python-4.7.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:7a4d325f39c1ae332b35f0238cb2aa6dfe4472a62a8c3b0956f0d96d8c15bec3", size = 1194303, upload-time = "2026-09-03T14:13:48.176Z" }, + { url = "https://files.pythonhosted.org/packages/e2/d5/ba876a02bcf8f329d755bbee070de8df080ab3434cfef7bdbc2e7dc0de7f/snowflake_connector_python-4.7.3-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:95412cad64a7fafaabb87aee4120489dafcef767dc6ac853090b6f86b8ed6496", size = 1206774, upload-time = "2026-09-03T14:13:49.539Z" }, + { url = "https://files.pythonhosted.org/packages/70/43/13cf1786f251129d0880f72e1185cc06c1833f27b3d973f913089ac7f431/snowflake_connector_python-4.7.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a101f32b1f2e0ed3d084503372d698e37290ef9ea211c29c916842802292dddb", size = 2919287, upload-time = "2026-09-03T14:13:27.391Z" }, + { url = "https://files.pythonhosted.org/packages/04/de/2dfd370d4796510f2f5a6e0db7e1e7ed6cba9ae82fb0bf83a876bff2f828/snowflake_connector_python-4.7.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ea6ebb8e2b782876210ed96535d95271cd3dee207f95f87f0ff3e80561bd24cc", size = 2951146, upload-time = "2026-09-03T14:13:29.267Z" }, + { url = "https://files.pythonhosted.org/packages/1a/ae/8492cd097065a890f9e1085acb207917a7ddf7a07a8fef83097f80e3f149/snowflake_connector_python-4.7.3-cp313-cp313-win_amd64.whl", hash = "sha256:1217e3426c2df6713ba1518ce908b68b5aadcff4fc9ad6760a8947a7561b8c07", size = 5431806, upload-time = "2026-09-03T14:14:03.166Z" }, + { url = "https://files.pythonhosted.org/packages/19/20/7842dba9ce00b6830d19223369891006bafbccbb0cc27e4f785337b1f62e/snowflake_connector_python-4.7.3-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:17bbac7513bc730ca07e78193af6a3c1a231639211a3b7e662411383282e174b", size = 1194878, upload-time = "2026-09-03T14:13:51.038Z" }, + { url = "https://files.pythonhosted.org/packages/2d/4f/23fbab0125c012fd7357616c37241fe3c2a0c20eb71e25570d1c6d036662/snowflake_connector_python-4.7.3-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:64cf70c154f54e3b78a1faedee4a281c2eec20aec16e86d26a75ac59122d077a", size = 1206534, upload-time = "2026-09-03T14:13:52.59Z" }, + { url = "https://files.pythonhosted.org/packages/1a/22/991627cbb0bcf1b4472843b532fdb24ccfaee75c35319d51acc113a7c3bd/snowflake_connector_python-4.7.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cbeee29b89a9350d0c6e18d28e76de7dc81e9ca5c72dc34a113a503f405d6bc", size = 2918192, upload-time = "2026-09-03T14:13:31.118Z" }, + { url = "https://files.pythonhosted.org/packages/84/4a/a95bafedf27f11efdf5d785278186cb510c325b457ba1e8f4958e08144ee/snowflake_connector_python-4.7.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a8b77a0add772037ef720e524f5ebc6913b6de282764d455990b8905eb7c6dfb", size = 2947257, upload-time = "2026-09-03T14:13:33.016Z" }, + { url = "https://files.pythonhosted.org/packages/5b/03/02e65809305a19ec6714c7f48e95089cf5fbe4473edc8641c4830aaffa7a/snowflake_connector_python-4.7.3-cp314-cp314-win_amd64.whl", hash = "sha256:44142aaf13d9a2674447a2ea1c0b310235cdc7964dcf92b7b64b9093b1edebfb", size = 5490657, upload-time = "2026-09-03T14:14:06.672Z" }, + { url = "https://files.pythonhosted.org/packages/9e/64/6376ae49f3a7cfeb8a2113e55143929762c5fb83571ce72b6da202098838/snowflake_connector_python-4.7.3-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:0348c697d2daaa9a5a6f5f505b11f3fb7d041fc828e64d8c7e4cdf3895fc0908", size = 1201255, upload-time = "2026-09-03T14:13:53.939Z" }, + { url = "https://files.pythonhosted.org/packages/a9/eb/805be55b52c37fa6e02a5014c7c1162bf54c5ec014189a772483fcc24a04/snowflake_connector_python-4.7.3-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:b6e9947ebb779aa775029fbb88e0691975171e4d93d2de33233b651794abd27e", size = 1211565, upload-time = "2026-09-03T14:13:55.281Z" }, + { url = "https://files.pythonhosted.org/packages/e7/c5/28d088a181fa9e912830eda1368a0cfb9946d92bb4ed0acc49fd079a1616/snowflake_connector_python-4.7.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2ab3fc5198914e9d0208ad168210b60a2fa1d25120f45fce6732811e94298752", size = 2960773, upload-time = "2026-09-03T14:13:34.762Z" }, + { url = "https://files.pythonhosted.org/packages/9d/35/36a0b7d85fc12ab4b357cbcdbc7772d926878544a1c4f62b3b7d30805970/snowflake_connector_python-4.7.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cbe8bd5f281616fd1394d36e3e633ba21e9d7b3ee02bbb2e32415e3214a89628", size = 2974895, upload-time = "2026-09-03T14:13:36.547Z" }, + { url = "https://files.pythonhosted.org/packages/d4/d6/0462d6ec1459ab0badc703c798d80132a2cbc79b234b6d25d2995da71e38/snowflake_connector_python-4.7.3-cp314-cp314t-win_amd64.whl", hash = "sha256:16bd99a500c603f7db01d454cf9882929dfbab2c7a2213706e5718229be2e379", size = 5494773, upload-time = "2026-09-03T14:14:04.783Z" }, +] + +[package.optional-dependencies] +secure-local-storage = [ + { name = "keyring" }, +] + +[[package]] +name = "snowflake-snowpark-python" +version = "1.55.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cloudpickle" }, + { name = "protobuf" }, + { name = "python-dateutil" }, + { name = "pyyaml" }, + { name = "setuptools" }, + { name = "snowflake-connector-python" }, + { name = "typing-extensions" }, + { name = "tzlocal" }, + { name = "wheel" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c6/35/84d01dd46679fd2324fca363e59599f52af32e5ee9b9fa68da17926f6690/snowflake_snowpark_python-1.55.0.tar.gz", hash = "sha256:2accc4e50f44635b8c9d5ed52f26a4d9885d00f17fcc16eee532e8d9d274c627", size = 1799269, upload-time = "2026-09-10T18:57:10.805Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d3/a4/d894e9fe0c541b3ad526709f16f1c1c8efc21bf1b80521e15284963e7dcb/snowflake_snowpark_python-1.55.0-py3-none-any.whl", hash = "sha256:dd5fd0c628d8227476706fbb50fa708f226fcc715ba7845e460780ba24c491c5", size = 1885909, upload-time = "2026-09-10T18:57:09.11Z" }, +] + +[[package]] +name = "sortedcontainers" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e8/c4/ba2f8066cceb6f23394729afe52f3bf7adec04bf9ed2c820b39e19299111/sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88", size = 30594, upload-time = "2021-05-16T22:03:42.897Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/46/9cb0e58b2deb7f82b84065f37f3bffeb12413f947f9388e4cac22c4621ce/sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0", size = 29575, upload-time = "2021-05-16T22:03:41.177Z" }, +] + +[[package]] +name = "sqlglot" +version = "30.18.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e4/73/5b5ce3e23b3ded3ea1986c2c2991217460d5fd5161b3e00a8425f5dcb8a3/sqlglot-30.18.0.tar.gz", hash = "sha256:e57e1b205e341979d1df5b1212c1435c598a0437e4619e3f428b15d5bc3a5cc6", size = 6018496, upload-time = "2026-09-03T13:12:58.206Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/08/9f/3dd2ec8dd84a33e0092f1c815267bc055073f9e833b93e389c59c10f62cc/sqlglot-30.18.0-py3-none-any.whl", hash = "sha256:ee0f9a9f3e2193e763c326e52dfb377b96fdb4292f6305c3ff2d9a220e71c601", size = 748788, upload-time = "2026-09-03T13:12:56.467Z" }, +] + +[[package]] +name = "starlette" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b5/b4/205b0d5241d934e8add0c38aa924c4f9fb7330834ff11e5444db964ec3f9/starlette-1.6.0.tar.gz", hash = "sha256:d4e3ac5e546444960c710297a3c9fc3f7ebae1b7e963f3d36173b49da535be9b", size = 2716969, upload-time = "2026-08-08T18:27:57.512Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/cb/6a6a47d5b464bd08695d254f3da6e7986cc70c9fa5d778eda57538edfe56/starlette-1.6.0-py3-none-any.whl", hash = "sha256:a86dd39d14bb45f85a3d18525215a9ef0cfd1f192ac793220e72598c90335f0c", size = 75969, upload-time = "2026-08-08T18:27:56.196Z" }, +] + +[[package]] +name = "toml" +version = "0.10.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/be/ba/1f744cdc819428fc6b5084ec34d9b30660f6f9daaf70eead706e3203ec3c/toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f", size = 22253, upload-time = "2020-11-01T01:40:22.204Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/44/6f/7120676b6d73228c96e17f1f794d8ab046fc910d781c8d151120c3f1569e/toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", size = 16588, upload-time = "2020-11-01T01:40:20.672Z" }, +] + +[[package]] +name = "tomlkit" +version = "0.15.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/96/e07752635b98536177fa1f37671c8f3cdde2e724c6bcf6034b2cfb571565/tomlkit-0.15.1.tar.gz", hash = "sha256:e25bbf38843005246210a12982776f27f99cb9be67160e14434d0c0d21ee1e97", size = 180129, upload-time = "2026-07-17T01:48:04.562Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/13/bc/8c13eb66537dce1d2bd3a57132902f38d0e7f5bb46fa9f4daed9fe9d76ee/tomlkit-0.15.1-py3-none-any.whl", hash = "sha256:177a05aece5a8ca5266fd3c448abb47b8d352f09d477d3ca8332db4d89b24304", size = 49449, upload-time = "2026-07-17T01:48:05.728Z" }, +] + [[package]] name = "typing-extensions" version = "4.16.0" @@ -303,3 +2252,516 @@ sdist = { url = "https://files.pythonhosted.org/packages/a3/26/b09b8010994eccc3c wheels = [ { url = "https://files.pythonhosted.org/packages/67/81/4add07e5172b7ac40d8ed5ff580409a7801a4fe26d529bdd915401dabfbe/typing_inspection-0.4.4-py3-none-any.whl", hash = "sha256:65b8397ba37ccbce054456aaccddfc91e6e3083c92824df348d96ca832f3f147", size = 14750, upload-time = "2026-08-12T12:37:24.648Z" }, ] + +[[package]] +name = "tzdata" +version = "2026.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e4/31/3d74fa778a63b98b7374323befcc0be5ab3bd94afd4096a0124e7379152c/tzdata-2026.4.tar.gz", hash = "sha256:f1b8bd365d8d210c55353f4d7f8d6d8561c0ba50d704b700d195a9424bba0d79", size = 199350, upload-time = "2026-09-12T12:56:03.251Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f9/bc/8737e8d54cf51106118039b83f485a4783112fab49ea9d044b234978a46e/tzdata-2026.4-py2.py3-none-any.whl", hash = "sha256:c2169a8b0a7a5e9674da5a135ccdfb2b3e671b333ed9fed17b41f73c34476e81", size = 347494, upload-time = "2026-09-12T12:56:01.67Z" }, +] + +[[package]] +name = "tzlocal" +version = "5.4.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "tzdata", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/81/5b/879b2f932adfa7a053c360d50bc896c977fa6426109185f7c12ebdd0cb9d/tzlocal-5.4.4.tar.gz", hash = "sha256:8dbb8660838688a7b6ba4fed31d18dedf842afb4d47ca050d6d891c2c15f3be4", size = 31170, upload-time = "2026-06-29T08:03:40.026Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/a4/017a7a6cbe387d961a688ec31364ae60a5c4e22c96ae9921b79a947c855d/tzlocal-5.4.4-py3-none-any.whl", hash = "sha256:aae09f0126a8a86fa736be266eb4a471380d26a0de3bc14844e7821fee3e2a15", size = 18115, upload-time = "2026-06-29T08:03:38.666Z" }, +] + +[[package]] +name = "urllib3" +version = "2.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e3/05/b17359e1cefb4f909b5e40b1b90a496d987258916dbbf88e842c729f510e/urllib3-2.8.0.tar.gz", hash = "sha256:63bf2ead4c879426ebf22ef2a781eeb4aa3b4ae798a0435506f8687fd5bb9b63", size = 458972, upload-time = "2026-09-15T19:29:36.253Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/92/9d/c4e665119135114480843e7ab388fa94d8480650450e6f8e26b70d323a4c/urllib3-2.8.0-py3-none-any.whl", hash = "sha256:0cf3cae568d36aa9576b28dfb35f11328f1cb974ca7647d9475ebb86c75ac6e3", size = 135717, upload-time = "2026-09-15T19:29:34.577Z" }, +] + +[[package]] +name = "uvicorn" +version = "0.53.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5d/ad/04bbb797c84fc1f26cb171f7394716f4865ffb8d8c5e1eef42565c2dfa6b/uvicorn-0.53.0.tar.gz", hash = "sha256:a9356f0cb89b3b8621529c5d5eebd69bfe154f4c3f68b4cf2de47e45fa855c2e", size = 110881, upload-time = "2026-09-14T07:44:23.815Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/18/0eea75741ee812e9f598b687619ce2454f6c3a1c5cd21ea990ec6bd26f45/uvicorn-0.53.0-py3-none-any.whl", hash = "sha256:e8dca71ec86dce5f04e333f0d56cdedf942446e6643b9cea1af0d6d3a02cb03e", size = 87081, upload-time = "2026-09-14T07:44:22.179Z" }, +] + +[package.optional-dependencies] +standard = [ + { name = "httptools" }, + { name = "python-dotenv" }, + { name = "pyyaml" }, + { name = "uvloop", marker = "platform_python_implementation != 'PyPy' and sys_platform != 'cygwin' and sys_platform != 'win32'" }, + { name = "watchfiles" }, + { name = "websockets" }, +] + +[[package]] +name = "uvloop" +version = "0.22.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/06/f0/18d39dbd1971d6d62c4629cc7fa67f74821b0dc1f5a77af43719de7936a7/uvloop-0.22.1.tar.gz", hash = "sha256:6c84bae345b9147082b17371e3dd5d42775bddce91f885499017f4607fdaf39f", size = 2443250, upload-time = "2025-10-16T22:17:19.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/d5/69900f7883235562f1f50d8184bb7dd84a2fb61e9ec63f3782546fdbd057/uvloop-0.22.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c60ebcd36f7b240b30788554b6f0782454826a0ed765d8430652621b5de674b9", size = 1352420, upload-time = "2025-10-16T22:16:21.187Z" }, + { url = "https://files.pythonhosted.org/packages/a8/73/c4e271b3bce59724e291465cc936c37758886a4868787da0278b3b56b905/uvloop-0.22.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3b7f102bf3cb1995cfeaee9321105e8f5da76fdb104cdad8986f85461a1b7b77", size = 748677, upload-time = "2025-10-16T22:16:22.558Z" }, + { url = "https://files.pythonhosted.org/packages/86/94/9fb7fad2f824d25f8ecac0d70b94d0d48107ad5ece03769a9c543444f78a/uvloop-0.22.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:53c85520781d84a4b8b230e24a5af5b0778efdb39142b424990ff1ef7c48ba21", size = 3753819, upload-time = "2025-10-16T22:16:23.903Z" }, + { url = "https://files.pythonhosted.org/packages/74/4f/256aca690709e9b008b7108bc85fba619a2bc37c6d80743d18abad16ee09/uvloop-0.22.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:56a2d1fae65fd82197cb8c53c367310b3eabe1bbb9fb5a04d28e3e3520e4f702", size = 3804529, upload-time = "2025-10-16T22:16:25.246Z" }, + { url = "https://files.pythonhosted.org/packages/7f/74/03c05ae4737e871923d21a76fe28b6aad57f5c03b6e6bfcfa5ad616013e4/uvloop-0.22.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:40631b049d5972c6755b06d0bfe8233b1bd9a8a6392d9d1c45c10b6f9e9b2733", size = 3621267, upload-time = "2025-10-16T22:16:26.819Z" }, + { url = "https://files.pythonhosted.org/packages/75/be/f8e590fe61d18b4a92070905497aec4c0e64ae1761498cad09023f3f4b3e/uvloop-0.22.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:535cc37b3a04f6cd2c1ef65fa1d370c9a35b6695df735fcff5427323f2cd5473", size = 3723105, upload-time = "2025-10-16T22:16:28.252Z" }, + { url = "https://files.pythonhosted.org/packages/3d/ff/7f72e8170be527b4977b033239a83a68d5c881cc4775fca255c677f7ac5d/uvloop-0.22.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:fe94b4564e865d968414598eea1a6de60adba0c040ba4ed05ac1300de402cd42", size = 1359936, upload-time = "2025-10-16T22:16:29.436Z" }, + { url = "https://files.pythonhosted.org/packages/c3/c6/e5d433f88fd54d81ef4be58b2b7b0cea13c442454a1db703a1eea0db1a59/uvloop-0.22.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:51eb9bd88391483410daad430813d982010f9c9c89512321f5b60e2cddbdddd6", size = 752769, upload-time = "2025-10-16T22:16:30.493Z" }, + { url = "https://files.pythonhosted.org/packages/24/68/a6ac446820273e71aa762fa21cdcc09861edd3536ff47c5cd3b7afb10eeb/uvloop-0.22.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:700e674a166ca5778255e0e1dc4e9d79ab2acc57b9171b79e65feba7184b3370", size = 4317413, upload-time = "2025-10-16T22:16:31.644Z" }, + { url = "https://files.pythonhosted.org/packages/5f/6f/e62b4dfc7ad6518e7eff2516f680d02a0f6eb62c0c212e152ca708a0085e/uvloop-0.22.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7b5b1ac819a3f946d3b2ee07f09149578ae76066d70b44df3fa990add49a82e4", size = 4426307, upload-time = "2025-10-16T22:16:32.917Z" }, + { url = "https://files.pythonhosted.org/packages/90/60/97362554ac21e20e81bcef1150cb2a7e4ffdaf8ea1e5b2e8bf7a053caa18/uvloop-0.22.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e047cc068570bac9866237739607d1313b9253c3051ad84738cbb095be0537b2", size = 4131970, upload-time = "2025-10-16T22:16:34.015Z" }, + { url = "https://files.pythonhosted.org/packages/99/39/6b3f7d234ba3964c428a6e40006340f53ba37993f46ed6e111c6e9141d18/uvloop-0.22.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:512fec6815e2dd45161054592441ef76c830eddaad55c8aa30952e6fe1ed07c0", size = 4296343, upload-time = "2025-10-16T22:16:35.149Z" }, + { url = "https://files.pythonhosted.org/packages/89/8c/182a2a593195bfd39842ea68ebc084e20c850806117213f5a299dfc513d9/uvloop-0.22.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:561577354eb94200d75aca23fbde86ee11be36b00e52a4eaf8f50fb0c86b7705", size = 1358611, upload-time = "2025-10-16T22:16:36.833Z" }, + { url = "https://files.pythonhosted.org/packages/d2/14/e301ee96a6dc95224b6f1162cd3312f6d1217be3907b79173b06785f2fe7/uvloop-0.22.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1cdf5192ab3e674ca26da2eada35b288d2fa49fdd0f357a19f0e7c4e7d5077c8", size = 751811, upload-time = "2025-10-16T22:16:38.275Z" }, + { url = "https://files.pythonhosted.org/packages/b7/02/654426ce265ac19e2980bfd9ea6590ca96a56f10c76e63801a2df01c0486/uvloop-0.22.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e2ea3d6190a2968f4a14a23019d3b16870dd2190cd69c8180f7c632d21de68d", size = 4288562, upload-time = "2025-10-16T22:16:39.375Z" }, + { url = "https://files.pythonhosted.org/packages/15/c0/0be24758891ef825f2065cd5db8741aaddabe3e248ee6acc5e8a80f04005/uvloop-0.22.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0530a5fbad9c9e4ee3f2b33b148c6a64d47bbad8000ea63704fa8260f4cf728e", size = 4366890, upload-time = "2025-10-16T22:16:40.547Z" }, + { url = "https://files.pythonhosted.org/packages/d2/53/8369e5219a5855869bcee5f4d317f6da0e2c669aecf0ef7d371e3d084449/uvloop-0.22.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bc5ef13bbc10b5335792360623cc378d52d7e62c2de64660616478c32cd0598e", size = 4119472, upload-time = "2025-10-16T22:16:41.694Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ba/d69adbe699b768f6b29a5eec7b47dd610bd17a69de51b251126a801369ea/uvloop-0.22.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1f38ec5e3f18c8a10ded09742f7fb8de0108796eb673f30ce7762ce1b8550cad", size = 4239051, upload-time = "2025-10-16T22:16:43.224Z" }, + { url = "https://files.pythonhosted.org/packages/90/cd/b62bdeaa429758aee8de8b00ac0dd26593a9de93d302bff3d21439e9791d/uvloop-0.22.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3879b88423ec7e97cd4eba2a443aa26ed4e59b45e6b76aabf13fe2f27023a142", size = 1362067, upload-time = "2025-10-16T22:16:44.503Z" }, + { url = "https://files.pythonhosted.org/packages/0d/f8/a132124dfda0777e489ca86732e85e69afcd1ff7686647000050ba670689/uvloop-0.22.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4baa86acedf1d62115c1dc6ad1e17134476688f08c6efd8a2ab076e815665c74", size = 752423, upload-time = "2025-10-16T22:16:45.968Z" }, + { url = "https://files.pythonhosted.org/packages/a3/94/94af78c156f88da4b3a733773ad5ba0b164393e357cc4bd0ab2e2677a7d6/uvloop-0.22.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:297c27d8003520596236bdb2335e6b3f649480bd09e00d1e3a99144b691d2a35", size = 4272437, upload-time = "2025-10-16T22:16:47.451Z" }, + { url = "https://files.pythonhosted.org/packages/b5/35/60249e9fd07b32c665192cec7af29e06c7cd96fa1d08b84f012a56a0b38e/uvloop-0.22.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c1955d5a1dd43198244d47664a5858082a3239766a839b2102a269aaff7a4e25", size = 4292101, upload-time = "2025-10-16T22:16:49.318Z" }, + { url = "https://files.pythonhosted.org/packages/02/62/67d382dfcb25d0a98ce73c11ed1a6fba5037a1a1d533dcbb7cab033a2636/uvloop-0.22.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b31dc2fccbd42adc73bc4e7cdbae4fc5086cf378979e53ca5d0301838c5682c6", size = 4114158, upload-time = "2025-10-16T22:16:50.517Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/f1171b4a882a5d13c8b7576f348acfe6074d72eaf52cccef752f748d4a9f/uvloop-0.22.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:93f617675b2d03af4e72a5333ef89450dfaa5321303ede6e67ba9c9d26878079", size = 4177360, upload-time = "2025-10-16T22:16:52.646Z" }, + { url = "https://files.pythonhosted.org/packages/79/7b/b01414f31546caf0919da80ad57cbfe24c56b151d12af68cee1b04922ca8/uvloop-0.22.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:37554f70528f60cad66945b885eb01f1bb514f132d92b6eeed1c90fd54ed6289", size = 1454790, upload-time = "2025-10-16T22:16:54.355Z" }, + { url = "https://files.pythonhosted.org/packages/d4/31/0bb232318dd838cad3fa8fb0c68c8b40e1145b32025581975e18b11fab40/uvloop-0.22.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:b76324e2dc033a0b2f435f33eb88ff9913c156ef78e153fb210e03c13da746b3", size = 796783, upload-time = "2025-10-16T22:16:55.906Z" }, + { url = "https://files.pythonhosted.org/packages/42/38/c9b09f3271a7a723a5de69f8e237ab8e7803183131bc57c890db0b6bb872/uvloop-0.22.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:badb4d8e58ee08dad957002027830d5c3b06aea446a6a3744483c2b3b745345c", size = 4647548, upload-time = "2025-10-16T22:16:57.008Z" }, + { url = "https://files.pythonhosted.org/packages/c1/37/945b4ca0ac27e3dc4952642d4c900edd030b3da6c9634875af6e13ae80e5/uvloop-0.22.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b91328c72635f6f9e0282e4a57da7470c7350ab1c9f48546c0f2866205349d21", size = 4467065, upload-time = "2025-10-16T22:16:58.206Z" }, + { url = "https://files.pythonhosted.org/packages/97/cc/48d232f33d60e2e2e0b42f4e73455b146b76ebe216487e862700457fbf3c/uvloop-0.22.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:daf620c2995d193449393d6c62131b3fbd40a63bf7b307a1527856ace637fe88", size = 4328384, upload-time = "2025-10-16T22:16:59.36Z" }, + { url = "https://files.pythonhosted.org/packages/e4/16/c1fd27e9549f3c4baf1dc9c20c456cd2f822dbf8de9f463824b0c0357e06/uvloop-0.22.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6cde23eeda1a25c75b2e07d39970f3374105d5eafbaab2a4482be82f272d5a5e", size = 4296730, upload-time = "2025-10-16T22:17:00.744Z" }, +] + +[[package]] +name = "watchfiles" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cd/41/5e1a4bb12aac5f1493fa1bdc11154eca3b258ca4eba65d39c473fe19d8e9/watchfiles-1.2.0.tar.gz", hash = "sha256:c995fba777f1ea992f090f9236e9284cf7a5d1a0130dd5a3d82c598cacd76838", size = 108252, upload-time = "2026-05-18T04:32:04.251Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fc/3d/8024c801df84d1587740d0359e7fdd80afeae3d159011f3d5376dd82f18e/watchfiles-1.2.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:704fd259e332e01f9b9c178f4bce9e49027e5587cc2600eeeaf8e76e1c846201", size = 400242, upload-time = "2026-05-18T04:31:19.014Z" }, + { url = "https://files.pythonhosted.org/packages/87/5b/f4dfd45323e949984a3a7f9dc31d1cbb049921e7d98253488dda72ccdaa9/watchfiles-1.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6543cf55d170003296d185c0af981f3e1311564907e1f4e08671fc7693a890a5", size = 394562, upload-time = "2026-05-18T04:30:08.46Z" }, + { url = "https://files.pythonhosted.org/packages/98/d8/19483ef075d601c409bce8bcbb5c0f81a10876fff870400568f08ce484a1/watchfiles-1.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:89d8c2394a065ca86f5d2910ff263ae67c127e1376ccc4f9fc35c71db879f80a", size = 456611, upload-time = "2026-05-18T04:30:45.723Z" }, + { url = "https://files.pythonhosted.org/packages/b1/6a/cc81fbe7ee42f2f22e661a6e12def7807e01b14b2f39e0ff83fd373fd307/watchfiles-1.2.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:772b80df316480d894a0e3165fdd19cf77f5d17f9a787f94029465ad0e3529d1", size = 461379, upload-time = "2026-05-18T04:31:29.292Z" }, + { url = "https://files.pythonhosted.org/packages/b1/57/7e669002082c0a0f4fb5113bb70125f7110124b846b0a11bc5ae8e90eac1/watchfiles-1.2.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d158cd89df6053823533e06fb1d73c549133bff5f0396170c0e53d9559340717", size = 493556, upload-time = "2026-05-18T04:30:05.44Z" }, + { url = "https://files.pythonhosted.org/packages/45/7d/f60a2b19807b21fe8281f3a8da4f59eef0d5f96825ac4680ba2d4f2ebf91/watchfiles-1.2.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d516b3283a758e087841aedb8031549fb41ced08f3db10aa6d2bf32dc042525b", size = 575255, upload-time = "2026-05-18T04:30:40.568Z" }, + { url = "https://files.pythonhosted.org/packages/bd/49/77f5b5e6efbcd57482f74948ebb1b97e5c0046d6b61475042d830c84b3ff/watchfiles-1.2.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:53b2290c92e0506d102cd448fbc610d87079553f86caa39d67440856a8b8bba5", size = 467052, upload-time = "2026-05-18T04:31:17.942Z" }, + { url = "https://files.pythonhosted.org/packages/ee/5a/73e2959af1b97fd5d556f9a8bdba017be23ceeef731869d5eaa0a753d5a3/watchfiles-1.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a711b51aec4370d0dcda5b6c09463206f133a5759341d7744b953a7b62e1100e", size = 456858, upload-time = "2026-05-18T04:30:30.182Z" }, + { url = "https://files.pythonhosted.org/packages/50/57/1bc8c27fad7e6c19bddee15d276dbb6ab72480ec01c127afff1673aee417/watchfiles-1.2.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:e2ca07fa7d89195ec0865d3d285666286740bfa83d83e5cee204043a31ecc165", size = 467579, upload-time = "2026-05-18T04:32:15.897Z" }, + { url = "https://files.pythonhosted.org/packages/09/6c/3c2e44edba3553c5e3c3b8c8a2a6dee6b9e12ae2cf4bd2378bebf9dc3038/watchfiles-1.2.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e0618518f282c4ebff60f5e5b1247b6d91bb8b9f4476947563a1e74acc66f3c6", size = 633253, upload-time = "2026-05-18T04:31:37.123Z" }, + { url = "https://files.pythonhosted.org/packages/30/c2/d8c84a882ab39bbefcc4915ab3e91830b7a7e990c5570b0b69075aba3faf/watchfiles-1.2.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0d191c054d0715c3c95c99df9b8dbf6fd096d8c1e021e8f212e1bd8bc444ccb5", size = 660713, upload-time = "2026-05-18T04:31:24.62Z" }, + { url = "https://files.pythonhosted.org/packages/a9/07/f97736a5fc605364fe67b25e9fa4a6965dfd4840d50c406ada507e9d735f/watchfiles-1.2.0-cp311-cp311-win32.whl", hash = "sha256:9342472aff9b093c5acd4f6d8f70ae0937964ab56542502bcf5579782da69ae8", size = 277222, upload-time = "2026-05-18T04:31:21.131Z" }, + { url = "https://files.pythonhosted.org/packages/cf/99/2b04981977fc2608afd60360d928c6aecf6b950292ca221d98f4005f6694/watchfiles-1.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:dbd6c97045dad81227c8d040173da044c1de08de64a5ea8b555da4aee1d5fa22", size = 290274, upload-time = "2026-05-18T04:31:45.966Z" }, + { url = "https://files.pythonhosted.org/packages/3c/74/f7f58a7075ee9cf612b0cfcddb78b8cd8234f0742d6f0075cf0da2dde1c6/watchfiles-1.2.0-cp311-cp311-win_arm64.whl", hash = "sha256:57a2d9fa4fb4c2ecae57b13dfff2c7ab53e21a2ba674fe9f05506680fcdcc0d7", size = 283460, upload-time = "2026-05-18T04:31:39.126Z" }, + { url = "https://files.pythonhosted.org/packages/b8/2f/e42c992d2afda3108ea1c02acecc991b9f31d05c14adc2a7cee9ee211fc4/watchfiles-1.2.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:bc13eb17538be00c874699dc0abe4ee2bc8d50bb1166a6b9e175ef3fd7eb8f26", size = 400115, upload-time = "2026-05-18T04:32:02.06Z" }, + { url = "https://files.pythonhosted.org/packages/5f/8f/6af2ea19065c91d8b0ea3516fdfc8c0d349f407e8e9fbf4e5a17360de8ad/watchfiles-1.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2d95ddc1eb6914154253d239089900813f6a767e174b8e6a50e7fdacb7e4236c", size = 393659, upload-time = "2026-05-18T04:30:50.951Z" }, + { url = "https://files.pythonhosted.org/packages/13/01/b32a967c56fb3e3e5be3db52c3d3b87fa4513aa367d8ed1ad96d42952e5f/watchfiles-1.2.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f70d8b291ef6e88d19b1f297a6905ddb978888d9272b0d05e6f53309856bcfc", size = 453207, upload-time = "2026-05-18T04:31:04.231Z" }, + { url = "https://files.pythonhosted.org/packages/04/98/97557a812180338cb1abd32e1cffcc4588f59b5f23e0cb006b2ba95ba64a/watchfiles-1.2.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:56d8641cf834c2836922899105bd3ce3d0dfc69291d52edf0b4d0436829b34c0", size = 459273, upload-time = "2026-05-18T04:31:50.377Z" }, + { url = "https://files.pythonhosted.org/packages/e8/a8/b4b08dcb7653b8087c6586f7ce649505900e866bbcfe40dc9587af02e686/watchfiles-1.2.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2581a94056e55d7d0a31a823ea92bf73749c489ca2285bfdc0fbe6b2bb49d50c", size = 489927, upload-time = "2026-05-18T04:31:42.485Z" }, + { url = "https://files.pythonhosted.org/packages/50/94/3dceea03545d2e5ddfd839f0ddd5e1cecbf1697b5a428d5ba11cef6af95d/watchfiles-1.2.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:41bc1199f7523b3f82843c88cbb979180c949caef0342cf90968f178e5d49b01", size = 570476, upload-time = "2026-05-18T04:31:03.071Z" }, + { url = "https://files.pythonhosted.org/packages/cc/f2/d39a5450c3532092b91f81d274360e613c2371bc874a89c7a1a3c5e8d138/watchfiles-1.2.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7571e4464cb6e434958f867f7f730b8ab0b75e3f8e5eac0499168486ab3c33a8", size = 465650, upload-time = "2026-05-18T04:30:12.701Z" }, + { url = "https://files.pythonhosted.org/packages/22/24/ed72f68cbc1333ca9b9f2200aa048bb6658ae41709bc1caad4310f4bdffd/watchfiles-1.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e53a384f76b631c3ae5334ce6a52f0baa3a911eb94a4eac7f160079868b716d5", size = 456398, upload-time = "2026-05-18T04:30:13.784Z" }, + { url = "https://files.pythonhosted.org/packages/0d/64/982ef4a4e5bab5b6e5b6becc8cd5e732f6130a78b855f0abec6439a9a135/watchfiles-1.2.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:d20029a60a71a052a24c4db7673bc4de39ab89adbaccbfb5d67987c5d73f424d", size = 465140, upload-time = "2026-05-18T04:31:52.111Z" }, + { url = "https://files.pythonhosted.org/packages/a0/0c/95282abf4ed680b6096010bcfc30c5fa7a041fc5aa5a2ad17a2cc6c75bba/watchfiles-1.2.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:2cb93af48550faf1cea04c303107c8b75833de7013e57ce27d3b8d21d8d0f58c", size = 630259, upload-time = "2026-05-18T04:31:25.676Z" }, + { url = "https://files.pythonhosted.org/packages/30/45/607c1de1530c4bdcf2cf1d1ecc2505ddba5d96bd43ba9f2b0e79876f850f/watchfiles-1.2.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2995c176de7692b86a2e4c58d9ec718f753150a979cb4a754e2b4ffa38e70906", size = 659859, upload-time = "2026-05-18T04:30:24.333Z" }, + { url = "https://files.pythonhosted.org/packages/fa/08/d9e2e0f9e8e6791d33aefc694ad7eefa7f901f63caff84a81ded38692f9c/watchfiles-1.2.0-cp312-cp312-win32.whl", hash = "sha256:7a2cffd17d27d2ecbb310c2b1d8174f222a5495b1a721894afa88ec11e25b898", size = 275480, upload-time = "2026-05-18T04:30:31.307Z" }, + { url = "https://files.pythonhosted.org/packages/1c/e6/9d42569c0102645cc8cea5d8c7d8a1e9d4ada2cb7f05f75e554b8aa2202a/watchfiles-1.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:f155b3a1b2a5fc89cdc70d47ee5d54e3b75e88efa34982028a35daef9ba00379", size = 288718, upload-time = "2026-05-18T04:32:10.745Z" }, + { url = "https://files.pythonhosted.org/packages/0a/26/88e0dc6ee3898169d7fa22bb6a69cabf2502d2ee25cb8c876d1262d204f8/watchfiles-1.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:8fa585ede612ee9f9e91b18bebf9ba11b9ae29a4e3a0d0cf6fca3e382133f0d5", size = 281026, upload-time = "2026-05-18T04:30:22.23Z" }, + { url = "https://files.pythonhosted.org/packages/d1/4d/70a7feced9f87e2ff26dba42667290f41694fc64646c67261fbb8cab5d5c/watchfiles-1.2.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:01ea8d66f0693b9b60a6541c8d10263091ca9a9060d242f3c1f3143f9aad2c98", size = 399730, upload-time = "2026-05-18T04:31:38.162Z" }, + { url = "https://files.pythonhosted.org/packages/31/3a/0da302f2307aee316922806ebd5726c542cbd787c938271cf14a074c7daf/watchfiles-1.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7ba0480b9a74af058f43b337e937a451e109295c420916d68ad24e3dc02f5e44", size = 392842, upload-time = "2026-05-18T04:30:27.051Z" }, + { url = "https://files.pythonhosted.org/packages/db/ef/d5bdb705c224dbc256aa0c1ec47bf4e61ec52558f2afb44a71a1fe4d7015/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4f34e26a19f91f710c08e0183429f0d1d15df734e6bc78c31e77b9ea9c433658", size = 452989, upload-time = "2026-05-18T04:31:11.945Z" }, + { url = "https://files.pythonhosted.org/packages/71/29/5495f2c1661949ef7a35e4d71111d129cfe7606414a26887a919d0a55406/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b4e77f6a55f858504069abd35d336a637555c09bca453dde1ee1e5ada8a6a1fb", size = 458978, upload-time = "2026-05-18T04:30:52.606Z" }, + { url = "https://files.pythonhosted.org/packages/d5/8c/7f9c07c433811c2fffd93e13fdfb7135de9aab5f2ae41be08960fa0047dc/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0cb4d80e212f116474a545c21c912b445f16bb0cef9e6a73a498164223e14e2f", size = 490248, upload-time = "2026-05-18T04:31:36.003Z" }, + { url = "https://files.pythonhosted.org/packages/3c/11/d93632febc52fbc21be90231bb7c17fd5387f46c9076fd40a5f9c2ae6910/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b974946a10af379d425e2eef5b62f5c6ebeaccf91d45eaad6f5b27ecd4f91aa0", size = 571847, upload-time = "2026-05-18T04:31:10.862Z" }, + { url = "https://files.pythonhosted.org/packages/55/b4/383173e73aabb07ad1d9c7aa859d95437ac46a6d6a1e11005facda0c9d19/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:86bc13c25a8d1fcd70b51d0ce7c9b65e90de5666fcbfd3e34957cc73ee19aeb5", size = 465974, upload-time = "2026-05-18T04:30:17.006Z" }, + { url = "https://files.pythonhosted.org/packages/a7/6c/89b1a230a78f57c52dd8893adb1f92f94411721b6ec12596c56d98c74356/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca148d73dea36c9763aaa351e4d7a51780ec1584217c45276f4fe8239c768b71", size = 454782, upload-time = "2026-05-18T04:30:35.656Z" }, + { url = "https://files.pythonhosted.org/packages/24/62/1732118367cfff0a9fce3bf62ff4bfded09ef5df21d9d446b858b3f70a96/watchfiles-1.2.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:c525543d91961c6955b2636b308569e84a1d1c5f5f2932041ab9ef46422f43e3", size = 465182, upload-time = "2026-05-18T04:30:20.846Z" }, + { url = "https://files.pythonhosted.org/packages/28/96/716f7e5f51339bf22963f3345f9f27d7f3b30e2eadc597e257c881dd3c53/watchfiles-1.2.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:a204794696ffb8f9b10fba6f7cb5216d42f3b2b71860ccac6b6e42f5f10973b0", size = 629841, upload-time = "2026-05-18T04:31:05.397Z" }, + { url = "https://files.pythonhosted.org/packages/4c/fe/c40783950fd771ccf66ab3ec2722d188a9af1c7f96c6e811f36e40c6e03f/watchfiles-1.2.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:10d86db20695afe7997ac9e1717637d6714a8d0220458c33f3d2061f54cec427", size = 658028, upload-time = "2026-05-18T04:31:48.22Z" }, + { url = "https://files.pythonhosted.org/packages/71/72/4508db1856d1d87fcbb3b63f4839bab1b5682cb0e8d224d122263c09654a/watchfiles-1.2.0-cp313-cp313-win32.whl", hash = "sha256:eb283ee99e21ad6443c8cdb06ac5b34b1308c329cbdf03fa02b445363714c799", size = 275183, upload-time = "2026-05-18T04:30:59.57Z" }, + { url = "https://files.pythonhosted.org/packages/f9/36/14b76ca57652e5cc5fd1c11f32a261292c08a0d19a00351013c2549cbfb2/watchfiles-1.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:a0f27f01bee51861392bb6b7c4fdb290b27d1eb194e9e28788d68102a0e898d9", size = 288059, upload-time = "2026-05-18T04:32:07.937Z" }, + { url = "https://files.pythonhosted.org/packages/1b/8d/0a85e395398d8d20fadfe5c5d32c726eee17a519e78fb356f2cf7531bffe/watchfiles-1.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:3651aa7058595e9cfb75d35dd5ada2bf9f48a5b8a0f3562821d3e210c507e077", size = 280186, upload-time = "2026-05-18T04:31:54.484Z" }, + { url = "https://files.pythonhosted.org/packages/37/68/36db056f1fdcc5f07302f56e631774d6835bcd6fa3ace402304621d5f9e5/watchfiles-1.2.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:faea288b6f0ab1902ef08f4ca6de005dccf856c4e0c4f21b8c5fce02d90a1b08", size = 399031, upload-time = "2026-05-18T04:30:44.576Z" }, + { url = "https://files.pythonhosted.org/packages/c1/64/01a9d6f66a82a5c101ce939274106cc72759d62427e153f01edd2b9f87c2/watchfiles-1.2.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:01859b11fd9fbca670f4d5da00fbac282cfea9bd67a2125d8b2833a3b5617ea9", size = 391205, upload-time = "2026-05-18T04:30:25.413Z" }, + { url = "https://files.pythonhosted.org/packages/84/2c/0a44fe058cb4bb7b8ede6b6670698bbb7c0400740e378d00022189b7b31d/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fff610d7bb2256a317bb1e96f0d7862c7aa8076733ee5df0fd41bbe76a24a4f4", size = 451892, upload-time = "2026-05-18T04:32:14.005Z" }, + { url = "https://files.pythonhosted.org/packages/67/a1/351e0d56cd35e6488b5c8b4fb11a809a5bc923e8fe8fed9faf8920be0c89/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b141a4891c995a039cd89e9a49e62df1dc8a559a5d1a6e4c7106d16c12777a55", size = 458867, upload-time = "2026-05-18T04:31:22.279Z" }, + { url = "https://files.pythonhosted.org/packages/d5/7d/9d09605187f1b838998624049fcf8bf47b73c1a3b76901fcac1782f62277/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f22943b7770483f6ea0721c6b11d022947a98eb0acae14694de034f4d0d38925", size = 490217, upload-time = "2026-05-18T04:31:43.657Z" }, + { url = "https://files.pythonhosted.org/packages/60/5d/a17a16eccb182f04188cd308ec24b1a71a9b5c4e7098269cf35d9fa56d02/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1bc6195825b7dcd217968bb1f801a60fd4c16e8eeab5bedc7fe917d7d5995ab4", size = 571458, upload-time = "2026-05-18T04:32:11.875Z" }, + { url = "https://files.pythonhosted.org/packages/d3/3d/4dd457062083ab1938e5dfd45032eb425cee2ac817287ca8ff4356183e5d/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d4a4b147f5dca2a5d325a06a832fb43f345751adfbc63204aec30e0d9ca965a2", size = 464707, upload-time = "2026-05-18T04:30:43.492Z" }, + { url = "https://files.pythonhosted.org/packages/c6/71/ea8c57b128f5383de74d0c7d2d9c57ad7c9a65a930c451bd25d524b295b7/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4543579a9bdb0c9560039b4ffddbdb39545707659fbc430ce4c10f3f68d557f9", size = 454663, upload-time = "2026-05-18T04:30:16.061Z" }, + { url = "https://files.pythonhosted.org/packages/53/fd/2e812bf938406d7db351f0703ddd3fc6c061cf30d96153a77bc79a943a44/watchfiles-1.2.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:20aa0e708b920bde876a4aa82dc7dd6ebea228a63a67cda6632c2fc87b787efa", size = 463537, upload-time = "2026-05-18T04:31:44.9Z" }, + { url = "https://files.pythonhosted.org/packages/86/56/d17a7f1dd1bc3035f1072694a551301272f1739c2d8e319c927cb9e29b38/watchfiles-1.2.0-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:d413349d565dab74297f2a63e84a097936be69bf8f3b3801f27f380e32040f44", size = 629194, upload-time = "2026-05-18T04:31:14.141Z" }, + { url = "https://files.pythonhosted.org/packages/be/06/f1ff66bf5cae50aa4062779a0ecd0bbaf15e466195719074078947d9a17d/watchfiles-1.2.0-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:f28b2725eb8cce327b9b3ab02415c853011dc55c95832fe90de6bc56f5315f72", size = 656194, upload-time = "2026-05-18T04:31:47.14Z" }, + { url = "https://files.pythonhosted.org/packages/e7/54/a9c7ea9a82a4ac65e7004c0a03920b5cdd2f9c3b678757d9cd425aa51d53/watchfiles-1.2.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:b8c8358484d5fa12ef34f05b7f4168eaf1932f408725ff6d023c33ec17bd79d4", size = 400205, upload-time = "2026-05-18T04:32:05.153Z" }, + { url = "https://files.pythonhosted.org/packages/aa/5d/c9ab3534374a4a67450696905d6ef16a04405448b8dc52bd752ae50423d4/watchfiles-1.2.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9f04b092229ad2c50126dd3c922c8822e51e605993764a33058d4a791ab42281", size = 392508, upload-time = "2026-05-18T04:30:54.849Z" }, + { url = "https://files.pythonhosted.org/packages/26/ca/1ad30103535cf0cecd7b993e8d50edc5351b1820e38f2d22e3df58962feb/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7a7ce236284f002a156f70add88efe5c70879cccbb658be0822c54b1306fc09d", size = 452448, upload-time = "2026-05-18T04:30:53.727Z" }, + { url = "https://files.pythonhosted.org/packages/37/a1/ceee2cdf2afbd715fa07758d39c9859513eae411b23196f7fd039e5feedd/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b9909cc2b48468b575eefa944919e1fe8a36c5849d5c7c168f80a8c1db69398e", size = 459605, upload-time = "2026-05-18T04:30:23.312Z" }, + { url = "https://files.pythonhosted.org/packages/e8/f6/421e30fd1cb3907a84ed92ab3f1983e37ba2dca015e9a894a048418417a2/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a37faaed405c67e28e6be45a1fa4f206ef5a2860f27c237db9fa30704c38242", size = 490757, upload-time = "2026-05-18T04:30:47.358Z" }, + { url = "https://files.pythonhosted.org/packages/41/b0/55ed1b97ed08be7bba6f9a541cac15f2a858e1d74d2b07b6da70a82aab00/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9649193aa27bd9ff2e80ff29bfaa93085496c7a3a377592823cc58b77ee88add", size = 568672, upload-time = "2026-05-18T04:30:38.915Z" }, + { url = "https://files.pythonhosted.org/packages/d1/cf/d8ae8a80dd7bafab395ea7681c10237311bbf34d37704a8c744e7cf31fc7/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e4ff8e37f99cf1da89e255e07c9c4b37c214038c4283707bdec308cb1b0ea1f", size = 464197, upload-time = "2026-05-18T04:30:09.914Z" }, + { url = "https://files.pythonhosted.org/packages/7c/8a/3076c496ca8dafe0e8cd03fcebdfc47be4b1174b4e5b24ff6e396e6b3af2/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:054dc20fd2e3132b4c3883b4a00d72fd6e1f56fdaf89fccd12e8057d74cd74d7", size = 453181, upload-time = "2026-05-18T04:30:14.829Z" }, + { url = "https://files.pythonhosted.org/packages/e5/10/9745e17c98e7b8a86454df0a3c7b5686bd650383f1e9f26e4ebcbd6cc0c0/watchfiles-1.2.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:e140ed30ebde76796b686e67c182cff10ea2fbab186fafd1560f74bb5a473a6e", size = 465109, upload-time = "2026-05-18T04:30:28.123Z" }, + { url = "https://files.pythonhosted.org/packages/8f/95/8ef4a95481d3e0cb52d62a06fa6e972e81424be2d9698b91a2fecca9904c/watchfiles-1.2.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:bb7e52ecf68ba46d22df23467b87cffeb2146908aa523ebfe803019618cfda06", size = 630653, upload-time = "2026-05-18T04:31:49.304Z" }, + { url = "https://files.pythonhosted.org/packages/fd/e4/3b3bf36b0f829b50c6ebcb8d031583863c59f923d6a6af3d485e470d0fac/watchfiles-1.2.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:23282a321c8baf9b3a3c4afff673f9fe65eb7fdc2338d765ccad9d3d1916a5ba", size = 657838, upload-time = "2026-05-18T04:31:06.497Z" }, + { url = "https://files.pythonhosted.org/packages/21/b1/6cbbb50c1f3002ab568777d44aa21206dfb8807a840990c4037523b51812/watchfiles-1.2.0-cp314-cp314-win32.whl", hash = "sha256:c0db965c5f79aa49fe672d297cf1febc5ad149b658594944f49a54a2b96270a7", size = 275108, upload-time = "2026-05-18T04:30:06.891Z" }, + { url = "https://files.pythonhosted.org/packages/92/45/190ce6db8dcb4536682cf75d3889ff1a27182a58cb519d343cb6d9ea63d8/watchfiles-1.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:71283b39fd17e5408eb123bd37aeecfd9d54c81fc184421943208aadb879d103", size = 288441, upload-time = "2026-05-18T04:32:12.901Z" }, + { url = "https://files.pythonhosted.org/packages/74/0d/3eae1c2313ab08378431d907c3f8095ecca00f3eda33111cf4f0f2591799/watchfiles-1.2.0-cp314-cp314-win_arm64.whl", hash = "sha256:c5c19526f4e54a00f2666a6c0e9e40d582c09e865055ea7378bf0009aab857b3", size = 280684, upload-time = "2026-05-18T04:31:26.902Z" }, + { url = "https://files.pythonhosted.org/packages/b1/75/fb64e6c25d6b5ca636d03df34ffb1c6e9873303e76d27967e045f8df088f/watchfiles-1.2.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:d73a585accffa5ae39c17264c36ec3166d2fad7000c780f5ef83b2722afb9dd2", size = 398857, upload-time = "2026-05-18T04:32:17.108Z" }, + { url = "https://files.pythonhosted.org/packages/73/4e/9f7adf01754cbf81843722ccfec169d8f26c69778281a302855cecd2ee08/watchfiles-1.2.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ae99b14c5f21e026e0e9d96f40e07d8570ebee6cafd9d8fc318354606daa7a28", size = 392413, upload-time = "2026-05-18T04:31:07.911Z" }, + { url = "https://files.pythonhosted.org/packages/47/c8/bec626bcc2d69f44b9acb24ce7d60ed7b16b73628eea747fcbd169d8edda/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4429f3b105524a10b72c3a819b091c495d2811d419c1e1e8df773a5a5974f831", size = 452409, upload-time = "2026-05-18T04:31:20.142Z" }, + { url = "https://files.pythonhosted.org/packages/00/b7/b6362068e81e7c556d155a34c35d40ac3ef42d747b06d7f6e5bf58e359c2/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:43d818978d06062d9b22c4fab2ebe44cf5213d42dc8e62bda8c2760cfa2eeb33", size = 458827, upload-time = "2026-05-18T04:32:06.219Z" }, + { url = "https://files.pythonhosted.org/packages/67/f8/9a813fa42afb1e0b4625e75f0479826644d3ee8dc287e093799bc01f390c/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b9f732dc58b2dbe69e464ccf8fff7a03b0dd0be439da4c0720d3558527d3d6b4", size = 490104, upload-time = "2026-05-18T04:31:56.034Z" }, + { url = "https://files.pythonhosted.org/packages/2f/bf/27dfb6094ca4c9aad21298b5525b6c53cb36121ee454331d05161e58d130/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8f200104103feb097de4cab8fe4f5dd18a2026934c7dea98c55a2f5fd6d5a33b", size = 571360, upload-time = "2026-05-18T04:31:57.133Z" }, + { url = "https://files.pythonhosted.org/packages/fb/39/44a096d67270ea93df91d33877dbe91fbda3aa4f8ec2edf799d93eda8736/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:63ac26eefbf4af1741247d6fb68b11c49a25b2f7413fbd318a83a12aaa9cf666", size = 464644, upload-time = "2026-05-18T04:30:57.33Z" }, + { url = "https://files.pythonhosted.org/packages/0e/80/c7472203bad6268e3ef1ad260739704847898938ad7ea8b63a5131f46b50/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c4997d4e4a55f0d02b6cde327322daf3a0400e5df6c6b15948994bf72497925", size = 454771, upload-time = "2026-05-18T04:30:48.736Z" }, + { url = "https://files.pythonhosted.org/packages/51/cf/3b10b268b4b7f0fc26e9debb5eef1998b515887840f444cd3ec80c688755/watchfiles-1.2.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:4c887eba18b7945ac73067a8b4a66f21cd46c2539b2bc68588f7be6c7eb6d26b", size = 463494, upload-time = "2026-05-18T04:31:33.826Z" }, + { url = "https://files.pythonhosted.org/packages/3d/3e/a4302545cd589262a0dc7d140e86f7688eba3f9c72776c27f7e23b8864c4/watchfiles-1.2.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:3416ff151bb6b5a8d8d11664974fbef4d9305b9b2957839ab5a270468fd8df30", size = 629383, upload-time = "2026-05-18T04:31:15.596Z" }, + { url = "https://files.pythonhosted.org/packages/db/99/d5649df0a9a410d45b7c882304d0b790903ac9b6e8f2cfd12114e0c6b9f2/watchfiles-1.2.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:0e831a271c035d89789cffc386b6aa1375f39f1cd25eb7ca0997e4970d152fc5", size = 656093, upload-time = "2026-05-18T04:31:58.707Z" }, + { url = "https://files.pythonhosted.org/packages/92/b9/362702539275019a54dd2e94511b31a9b89c5f9e6a21966de7eb692549fc/watchfiles-1.2.0-cp315-cp315-macosx_10_12_x86_64.whl", hash = "sha256:37a6721cdf3f65dbb13aa9503510ccb4451603ac837e44d265d7992a597e1374", size = 400109, upload-time = "2026-05-18T04:31:16.879Z" }, + { url = "https://files.pythonhosted.org/packages/8f/75/71d5ba62db781e5587bded1d944c675374bc4aa37ff33d5018d98e8b6538/watchfiles-1.2.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:2b37d10b5a63bd4d87e18472d80fa525bd670586fae62e5dd580452764879b65", size = 392167, upload-time = "2026-05-18T04:31:28.058Z" }, + { url = "https://files.pythonhosted.org/packages/3c/01/c66dd95d0423fe30d31820e2d1d5bda773764131bbb6ac0cb1cf303ac328/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a105bc2283f67e8fbec74253ec2d94925de92ed72c0393f1206bf326b7b7b69", size = 452372, upload-time = "2026-05-18T04:31:00.836Z" }, + { url = "https://files.pythonhosted.org/packages/91/15/2fe99557e72f85627c6a8eed50d889e8d101623e060a22ad75b875cb932d/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5327989a465505f05cfe06f04fa9d0c2fd5432bb243e10e6f012b1bdca3c8579", size = 459596, upload-time = "2026-05-18T04:31:34.96Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/d4acfa0023367428ed48351b3b9b267893037b6cadae55620c61c24bcfd4/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ecb47f183a8025b2aa18b546725c3657e542112ae9c0613a2af79b4fa8d04ad7", size = 490869, upload-time = "2026-05-18T04:31:59.923Z" }, + { url = "https://files.pythonhosted.org/packages/a4/5f/3164cbdce06c9fb95c4f7b9e2f9760b5e2797af43a9ecc317ef42a23a278/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8520a4ab0e37f770afc34459c4f8f7019e153f9124dc101c15538365875d1ab2", size = 571641, upload-time = "2026-05-18T04:32:00.948Z" }, + { url = "https://files.pythonhosted.org/packages/41/e6/85d3731c55e65cd7690f3f803d24c139588aaf863e4bf2148fe7a7fa1a19/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:71cd71740ed2c15211ebb237ced4e39a1cdf6f80566e5fe95428da1626f4fde6", size = 464444, upload-time = "2026-05-18T04:30:34.298Z" }, + { url = "https://files.pythonhosted.org/packages/f4/7d/562641012b8b09872742c3b8adf9629ec479fd78f8d68ae4a0c13da8add6/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f88af53d6ddaf72179ef613ddc905e6f4785f712b49b80b3bef9f3525e6194b4", size = 453593, upload-time = "2026-05-18T04:31:23.464Z" }, + { url = "https://files.pythonhosted.org/packages/56/fe/cb8ef3d6f929d14158fdaaad9925985b7310abc9384dcd4d82dd0016fb59/watchfiles-1.2.0-cp315-cp315-manylinux_2_31_riscv64.whl", hash = "sha256:cee9d5efd929efdac5f7e58f72b3376f676b64050a91c5b99a7094c5b2317488", size = 465096, upload-time = "2026-05-18T04:31:30.384Z" }, + { url = "https://files.pythonhosted.org/packages/25/91/80908e835e100527a9267147b08c0eee1fa6ab0ffec15edc04d1d44885f7/watchfiles-1.2.0-cp315-cp315-musllinux_1_1_aarch64.whl", hash = "sha256:b718bf356bbc15e559bd8ef41782b573b8ae0e3f177ab244b440568d7ea02cfb", size = 630638, upload-time = "2026-05-18T04:30:49.89Z" }, + { url = "https://files.pythonhosted.org/packages/46/4b/95ab2f256bb4af3cb2eb23b9317bda984ee6e0f11733a5c004a6c95b06e3/watchfiles-1.2.0-cp315-cp315-musllinux_1_1_x86_64.whl", hash = "sha256:922c0e019fe68b3ae392965a766b02a71ba1168c932cebc3733cd52c5fe5b377", size = 657684, upload-time = "2026-05-18T04:31:32.027Z" }, + { url = "https://files.pythonhosted.org/packages/23/f4/7513ef1e85fc4c6331b59479d6d72661fc391fbe543678052ac72c8b6c19/watchfiles-1.2.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:4674d49eb94706dfe666c069fc0a1b646ffcf920473492e209f6d5f60d3f0cc2", size = 403050, upload-time = "2026-05-18T04:30:36.753Z" }, + { url = "https://files.pythonhosted.org/packages/27/0b/a54103cfd732bb703c7a749222011a0483ef3705948dae3b203158601119/watchfiles-1.2.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:094b9b70103d4e963499bdea001ee3c2697b144cd9ae6218a62c0f89ec9e31db", size = 396629, upload-time = "2026-05-18T04:32:03.268Z" }, + { url = "https://files.pythonhosted.org/packages/5e/2c/73f31a3b893886206c3f54d73e8ad8dee58cdb2f69ad2622e0a8a9e07f4e/watchfiles-1.2.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b0ef001f8c25ad0fa9529f914c1600647ecd0f542d11c19b7894768c67b6acb7", size = 457318, upload-time = "2026-05-18T04:31:01.932Z" }, + { url = "https://files.pythonhosted.org/packages/e9/f9/45d021e4a5cc7b9dd567f7cbb06d3b75f751a690063fb6cc7ec60f4e46b7/watchfiles-1.2.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a88fc94e647bc4eec523f1caa540258eb71d14278b9daf72fa1e2658a98df0f0", size = 457771, upload-time = "2026-05-18T04:30:56.331Z" }, +] + +[[package]] +name = "wcwidth" +version = "0.8.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/36/57/ed58088fafdf4c55a0ad6bde846502567645424d7ebf325230b9237f4085/wcwidth-0.8.3.tar.gz", hash = "sha256:d128512515fbf4612e0ff21fd6380399210318b7b54a9af59dff8454cf9730eb", size = 1458450, upload-time = "2026-08-28T18:10:06.875Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c4/0e/57f6bb3024a597b2e8ec4aee710ffe62ddc95af2e2bb1ee7a7abdc22c68c/wcwidth-0.8.3-py3-none-any.whl", hash = "sha256:d5b73dba6158a595ec9370350e7f2637bcac8d6c5e4fde34f30fcffb6103a5e4", size = 331669, upload-time = "2026-08-28T18:10:04.909Z" }, +] + +[[package]] +name = "websockets" +version = "17.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/18/72/fba934cb3dff7a85d811820efffcd141ddd52b5a2a01637f64551373ff4d/websockets-17.1.tar.gz", hash = "sha256:acfea4c20bf54384883ea33b1240fc1db4f52e190823a4e2b334bc3e8bfca96a", size = 187520, upload-time = "2026-08-26T17:25:33.063Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7d/ad/66a74d42fb537bd44056483eae6cbb7ebb10b742c300a0bf8cee427556d4/websockets-17.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:88b882764ef65147a7a5ae13168dedbe225a04e2ff4858fe543f2c402f093e9c", size = 216984, upload-time = "2026-08-26T14:55:20.747Z" }, + { url = "https://files.pythonhosted.org/packages/70/1b/344ab22cea729e872f759b926441f7b822ab6cd106db527736afc066927f/websockets-17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:98a5b2589a56a4b4f098b0a958099a4356ab904a7844f1da3841efca469af7e9", size = 214667, upload-time = "2026-08-26T14:55:22.298Z" }, + { url = "https://files.pythonhosted.org/packages/2e/42/bace574b6ae80e1a8d6935b8c5f03fb67236233ec572e976fe826ff719cf/websockets-17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:020e271205f8ab3406d7a59cd00de6dec722315924411c421bd00642f18bad86", size = 214944, upload-time = "2026-08-26T14:55:23.618Z" }, + { url = "https://files.pythonhosted.org/packages/ee/87/08e35ca4a0ffafb500a16ff461bf9561ad2b755362adb5d077d4dba9affc/websockets-17.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:65be6bda2b537fefa4b3a5ccd6ab386533ce39dd8fe62433ec90901fdc81752d", size = 224004, upload-time = "2026-08-26T14:55:24.748Z" }, + { url = "https://files.pythonhosted.org/packages/5c/9a/00ae2e147eaa086fe8bdddd36f57216ce72b9a9dfc0b17c717005ebdacaf/websockets-17.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0c84bdef916556cbe1d5a43b423398be4dd3cba6522b463e53d848578b920695", size = 224278, upload-time = "2026-08-26T14:55:26.016Z" }, + { url = "https://files.pythonhosted.org/packages/98/e2/7aeb4e00defa68826f449392922a382ce7fdf542fe52190558dc1714e284/websockets-17.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:47a62d6045c6eaa0d8f97bc2fb68b8cf90077a0cbfd4e83d6f2d2145611ee134", size = 225511, upload-time = "2026-08-26T14:55:27.183Z" }, + { url = "https://files.pythonhosted.org/packages/7e/e4/655be3d93c3edbe1a51606073b5454ea6b1b32d87aa26253a6df952417b7/websockets-17.1-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:34879e19bb0a3c44f9317679435aea5327fac993933a704cbf353bf1234b10c7", size = 228802, upload-time = "2026-08-26T14:55:28.431Z" }, + { url = "https://files.pythonhosted.org/packages/e4/33/98549a2afa9d68fe1b5a8e0a61cd461a43ea1ab7209bce675eea67c79190/websockets-17.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2d72879819f5145a342d0030c418702496c65a4b913ef81f5ae944dd91dd50f6", size = 226075, upload-time = "2026-08-26T14:55:29.695Z" }, + { url = "https://files.pythonhosted.org/packages/8e/6b/cbc27e014d6c292b9b2709cfd32781a2b61eb30cd4c9130e7c57e41a204a/websockets-17.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:84f25e099fdfe3b09f953d84698f729a1f7d1e99101b2787d7a28ed77b323750", size = 224846, upload-time = "2026-08-26T14:55:30.964Z" }, + { url = "https://files.pythonhosted.org/packages/10/a6/e57925f7a423d90f24559e85bac21a7f0b44c0cf4a5c0babc0759ca54bab/websockets-17.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:469355ab1af100b9380f1afb09985019f4a4b94fa1dd0e9396db4361626d7ab8", size = 222136, upload-time = "2026-08-26T14:55:32.376Z" }, + { url = "https://files.pythonhosted.org/packages/32/07/b9de0400addb542ba7c819022abc3afa46cd7e518068881bceadac69d995/websockets-17.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:00679b7468b4c2b12b0757118174e8eabac56bb2f579a928a104d9554a56e098", size = 225000, upload-time = "2026-08-26T14:55:33.527Z" }, + { url = "https://files.pythonhosted.org/packages/4c/bb/af2828a1d7f2beb792af6ba56d7b02d56262070266b95d2af9ef391fbfb0/websockets-17.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:a9fe648abd1d9b89aebfa30407bfdd08a0271ec5dc7d44a4c6ccd1ce22cf562a", size = 223592, upload-time = "2026-08-26T14:55:34.636Z" }, + { url = "https://files.pythonhosted.org/packages/fe/51/7379f254730c1dc7d8e4dd8d686868d8f7be55bdc94c6d3a44538840f639/websockets-17.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f47aafd92aa28b941180e6da8a42b0f711851b14b81a5b6bb28dbbb1fa35152c", size = 224360, upload-time = "2026-08-26T14:55:35.777Z" }, + { url = "https://files.pythonhosted.org/packages/88/c6/fcd91320dd71dda7046df9bc60f60c70ee15c052dee21e31ed6221dc8b5d/websockets-17.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:c89406fa3dcd4aa8662c6406cc5c0de1790e9614d2c3aaf03ca53a8a8ccf3405", size = 225404, upload-time = "2026-08-26T14:55:36.85Z" }, + { url = "https://files.pythonhosted.org/packages/1b/b2/655a4f939388079f80f1b3f8a1b9d40783e70a376e7423988cc9a590a09f/websockets-17.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:b3b451fd2723ad3191a209afe6f3f4bc86c83e9a85bdc255353b91803ee6aa66", size = 222982, upload-time = "2026-08-26T14:55:38.006Z" }, + { url = "https://files.pythonhosted.org/packages/87/75/37c84c4371c6aa668910d7841036c4397ea10554c07030603ccd5e44b02a/websockets-17.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:054c28db2dcec0e857e3b705d8c28012613e555b38c765d6a4f75340a4fc06a0", size = 224017, upload-time = "2026-08-26T14:55:39.39Z" }, + { url = "https://files.pythonhosted.org/packages/5e/20/8a9a94323bfcfe03bde3f9d98926bea4855856359702fe3ca0d07051ef5d/websockets-17.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f8e822efd54137d8cc8310eb64635ab827a4a6c72ff08691f38aa624776d8ecb", size = 224252, upload-time = "2026-08-26T14:55:40.498Z" }, + { url = "https://files.pythonhosted.org/packages/3d/dd/aa66e6500188cd40306abeb92c9a738ca6dd7029d8d8532c538055ab5daf/websockets-17.1-cp311-cp311-win32.whl", hash = "sha256:dcb8d5f7edef7a399d322cf28d4c4e6f98dab64d301c8f50581a1080e5198142", size = 217484, upload-time = "2026-08-26T14:55:41.763Z" }, + { url = "https://files.pythonhosted.org/packages/01/a2/cdf3b551f0b9177023afd3a45d3b431a0d4064951008c4321a8b42ac2288/websockets-17.1-cp311-cp311-win_amd64.whl", hash = "sha256:b1bc819c6db90e8f91a38250a1ab4c058261871aa52d2fe36382eddedf146dee", size = 217779, upload-time = "2026-08-26T14:55:42.942Z" }, + { url = "https://files.pythonhosted.org/packages/e0/13/51253dbed7d16a4bb87b05110ad3bf12165f410e915f6da1edd4186d8dc1/websockets-17.1-cp311-cp311-win_arm64.whl", hash = "sha256:edadce7a22052056fd4384543019856b34850363c9d387929f677ae01d79709c", size = 217710, upload-time = "2026-08-26T14:55:44.016Z" }, + { url = "https://files.pythonhosted.org/packages/a6/0d/098f23c4c858e5de9459ffc554fa07d5493fbcfca7f040b5800cf1cecc35/websockets-17.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:76dd004f59115087c7b700474cb18f01325e37250032e19396c08ae41448e4b3", size = 217015, upload-time = "2026-08-26T14:55:45.194Z" }, + { url = "https://files.pythonhosted.org/packages/13/86/bc1317b1a4d8c4688e2a7e564b5e004dab44c2534d7ca05de6ae9a863fca/websockets-17.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:581fa678ef46f4277cc8491312468e582f8ad609dbab907ba6096a08c6a0ff98", size = 214692, upload-time = "2026-08-26T14:55:46.366Z" }, + { url = "https://files.pythonhosted.org/packages/8f/e7/df821761772beaa48c211ee0e234930b35c1473778470773823f56d3911b/websockets-17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:87f0d5e77548b0c40c8464cdb6108792e7e53f487c6400028a4ec28a8afbe5ab", size = 214959, upload-time = "2026-08-26T14:55:47.885Z" }, + { url = "https://files.pythonhosted.org/packages/3e/92/c3fb72f11764812fc648bf3838d224972427b348e8b3989d9e0a9df87da3/websockets-17.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:882af300d2c6a092b93767d5de03c7bb56dfb06314140c8e872d3f48e09f7b74", size = 224278, upload-time = "2026-08-26T14:55:49.241Z" }, + { url = "https://files.pythonhosted.org/packages/fb/05/9f82d090c8d2d861604147ef6dfb938a90b039f9358d5193f1df62558593/websockets-17.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0c863507ada5805517ca6dff1c524dcd42942efe6304dacf06700878398d21a6", size = 224557, upload-time = "2026-08-26T14:55:50.348Z" }, + { url = "https://files.pythonhosted.org/packages/8a/50/5cbf677b865290fe36819ff00615826e7edc1df38786f770123ff39a933d/websockets-17.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d41ef69d5416fbc1d98cf96c37be6192d10fd101c3e0f8b3ddc36e09432b3c08", size = 225791, upload-time = "2026-08-26T14:55:51.75Z" }, + { url = "https://files.pythonhosted.org/packages/c1/1c/eb8a032285243381b09a221ae384c972d5000453ad136add4d1595cec798/websockets-17.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5aefe78e6a3077fe22b5e64b04666a85a3eb8b934d40e8595a693adcbceb6f11", size = 228574, upload-time = "2026-08-26T14:55:52.922Z" }, + { url = "https://files.pythonhosted.org/packages/69/85/413736251cb3ac04ce84cbd90e893d9a36a9698d4820b323aff3aa187e50/websockets-17.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f64e001bb7fa89b9f32cfa600bf8e9ac8ca26759d9b92ae01453ee303d9cd7b4", size = 226428, upload-time = "2026-08-26T14:55:54.263Z" }, + { url = "https://files.pythonhosted.org/packages/d2/2b/a08bcc7fa1ca81a10f84ba32b6e6edd73a913f4b0c2640eed1fd626efacd/websockets-17.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:677014a073bcb1fbaa7e21144786864f16c08f856d66834f611eceb9006cbab8", size = 225184, upload-time = "2026-08-26T14:55:55.943Z" }, + { url = "https://files.pythonhosted.org/packages/e5/8a/3bd2d0cf6b148c8c866d5d9fdcde30c04bfd81fdfac86813e69377eb4448/websockets-17.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0de501b7f2db11e83739ac20e2d33d46da4604b829f506c24be80e7def069391", size = 222430, upload-time = "2026-08-26T14:55:57.103Z" }, + { url = "https://files.pythonhosted.org/packages/3b/c9/8e891ae342668735eabbbc669895e15195e4b45f24a4beeb58af76f414c7/websockets-17.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f62114a54117e4948a1e414e89521f7fe1e3c2f83f2a571a06a4fc6718b0900a", size = 225227, upload-time = "2026-08-26T14:55:58.375Z" }, + { url = "https://files.pythonhosted.org/packages/e1/6f/c816f332dca11425e9bda7c07f7573eb5c5f8a735849d02b0d81e8ee20fa/websockets-17.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:eec113a5b41d124ef42ff56b0d74a6da3fd986400038eab9e58ee42a4024e837", size = 223831, upload-time = "2026-08-26T14:55:59.664Z" }, + { url = "https://files.pythonhosted.org/packages/53/67/5e91d5308ce24fc1ec74f56536c12f4888bad45ff5ea50f3180f8c518c57/websockets-17.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5f051f8030a51815dc00e24bd2e5f1435af095c1cc111d747ac6e2a3620d7641", size = 224600, upload-time = "2026-08-26T14:56:00.873Z" }, + { url = "https://files.pythonhosted.org/packages/bb/96/faa298ecf2570d35b0eb37caddf4992178d907e108ed74bfffb6bc092c29/websockets-17.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:655a8e28010f09fd6fa317e857afab3af7647f33e41dee88fa421e92086d1090", size = 225707, upload-time = "2026-08-26T14:56:02.001Z" }, + { url = "https://files.pythonhosted.org/packages/0b/12/5710d2482ca5061c1eec5eb46f6313837c760d4115b1795c85b6c08be4e3/websockets-17.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:dc2b79afc074d2f3e64b26539350f697fe1b85ea1c49ea24eb588f247b053ce1", size = 223263, upload-time = "2026-08-26T14:56:03.092Z" }, + { url = "https://files.pythonhosted.org/packages/27/47/0c30f4eebfd1d93fae779d268f678d48847fb98516f5200849574eee8820/websockets-17.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:e4bd7eacb87d8cf3ed70d6392c770a0d92441f05d7d2a3efafb5bc171d5e3067", size = 224244, upload-time = "2026-08-26T14:56:04.321Z" }, + { url = "https://files.pythonhosted.org/packages/41/33/46c256195a1255079ae23d1b1267b2e1843dc5f46a67f973cdf2a3523dff/websockets-17.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ccbf3f4a9890d50b3a08ee04029fde30a03bfdeffaa19977628bf17251764e60", size = 224520, upload-time = "2026-08-26T14:56:05.521Z" }, + { url = "https://files.pythonhosted.org/packages/06/9a/aef0792731df4352e5f417369b532b3325fe434765ca90c193f594ae1e67/websockets-17.1-cp312-cp312-win32.whl", hash = "sha256:7e724f843fa6a0614aece65a7c73e51d0f4412ca41dccac13c3caf98e69536bb", size = 217485, upload-time = "2026-08-26T14:56:06.715Z" }, + { url = "https://files.pythonhosted.org/packages/50/23/493ecfdaf32898e5ea24dc900e33e5e317f9662d5d9ab2d44b2e111b4e1c/websockets-17.1-cp312-cp312-win_amd64.whl", hash = "sha256:617243e19a0992095956f406ee9cd3bc4ba92862d83cb1d83bb59ce574412bec", size = 217786, upload-time = "2026-08-26T14:56:08.055Z" }, + { url = "https://files.pythonhosted.org/packages/97/3d/91954e2f7876f74ce1213e9b92c65a63b559cc4b942a931ebeb351cd9932/websockets-17.1-cp312-cp312-win_arm64.whl", hash = "sha256:9f4a08ff7cb68c27b18e09223cc6304e01d0f82d5a240d251266dfd2e6e44729", size = 217711, upload-time = "2026-08-26T14:56:09.267Z" }, + { url = "https://files.pythonhosted.org/packages/1d/31/5f6450a7879f4f063ef08897cc385ea3ce3f1fe17f08b11e3fd959abdf27/websockets-17.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2a0162a6372110a5601cb5c9fd826635cedf69f3e110c545dd19774e040b970e", size = 217006, upload-time = "2026-08-26T14:56:10.509Z" }, + { url = "https://files.pythonhosted.org/packages/d0/2a/c1b006fc861695d2aa4e35327b842015ce1d98cf8f99241829b3d6460bfc/websockets-17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:829dba1bc049779de9b332088c1a6a9858e96bd67e50b6b644a95e02b67836bc", size = 214690, upload-time = "2026-08-26T14:56:11.681Z" }, + { url = "https://files.pythonhosted.org/packages/46/69/66e5b7d01445e0eeb1d4ab419c30315f2c90cf7a8a8cd4ecc47f894dba54/websockets-17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:fd8f47dbf2e8adb15c847215f83436de3fdb120b51fdae0fbbdf69fd97a3ad80", size = 214947, upload-time = "2026-08-26T14:56:12.923Z" }, + { url = "https://files.pythonhosted.org/packages/07/ce/033cafe2d2538562efa876b9149a2c7a0f7787870a4b1bb6e28adc9ceb6b/websockets-17.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9f4c0377a83e163a303514fdfab501dbe379bdc13e5b9312a91d112658b29dce", size = 224329, upload-time = "2026-08-26T14:56:14.212Z" }, + { url = "https://files.pythonhosted.org/packages/34/c7/e1c2e8a67f6cc0aa43abe0046fb3b7a020980649e6a843751dc7ce9eb170/websockets-17.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c3241d684a76eaaef8b2dc789afde4343cd3aad55ea81e4e8ab3605b529bae51", size = 224611, upload-time = "2026-08-26T14:56:15.702Z" }, + { url = "https://files.pythonhosted.org/packages/be/de/07c6d48eb3d2069709410c851e7de10ab83d752c4bd09862899627c2729b/websockets-17.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e5f5c7a893507d0e83a80b88aefd6522f7e882cd53f9722c6f23f5a020c9557c", size = 225848, upload-time = "2026-08-26T14:56:16.962Z" }, + { url = "https://files.pythonhosted.org/packages/f3/dd/3c68572d20509648cc2fb6f50ccf3deeb4b87270f2c8966e99476e278ea3/websockets-17.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:00bf34b64501e3477e81fc281532ff3cbf4da26633c10b63979d5085d46602d3", size = 227290, upload-time = "2026-08-26T14:56:18.204Z" }, + { url = "https://files.pythonhosted.org/packages/0a/4a/8f6651c8a22093539c9215af0c5bbf217b87b382c99d2112039b92d593c2/websockets-17.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ce0305b702b20d1e1d60a9aaace6bc89970e1753565543f310d549eab22c2435", size = 226476, upload-time = "2026-08-26T14:56:19.459Z" }, + { url = "https://files.pythonhosted.org/packages/f5/be/f6fc33cea86b1127fd1297b18c107e81580ab55a73a39f9a934441ef321f/websockets-17.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29176d8b429cfa0fa443c473878d37a5c06cfd0cb36b71ba4314accc71e05906", size = 225233, upload-time = "2026-08-26T14:56:20.939Z" }, + { url = "https://files.pythonhosted.org/packages/cb/83/65edaf05f7c9b1dea82f4d252fdc37706a84571646f06119a27b0a16fe19/websockets-17.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3709a1ab30b4b922027d22f68d2b61a0656a91680ac894a537624e6be7dd7f7c", size = 222488, upload-time = "2026-08-26T14:56:22.208Z" }, + { url = "https://files.pythonhosted.org/packages/07/42/d1169c2f7f1f0032b0d4b0c00f0711a070cd7c735de37bfeb876bc0f9606/websockets-17.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:43bd0c1ceb924d67f5c1a5254d8361dd9d94246e6331a726064dfa2917880780", size = 225295, upload-time = "2026-08-26T14:56:23.445Z" }, + { url = "https://files.pythonhosted.org/packages/a6/f4/64e2a386c3899b917c2933225c9b47887874229d159797f3bf1a11c20d51/websockets-17.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:1fce0f43e0d41422e0b2cad6561e1970df22f212f4c7e884967df7cf591b031c", size = 223891, upload-time = "2026-08-26T14:56:24.647Z" }, + { url = "https://files.pythonhosted.org/packages/26/b3/dfb5c482f7e310a3432fdbb045ddfe6d34114680e89a233d4ff900a32961/websockets-17.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4031152769179ab8dcdeafc7b0e58052a49117560a28671700b47b2c7b717aad", size = 224661, upload-time = "2026-08-26T14:56:26.027Z" }, + { url = "https://files.pythonhosted.org/packages/a4/cf/94865130a336029f46412adc127c4fbe380f46172b90ce251369e35c4302/websockets-17.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:a06f3b5085176763182449559e20391d7ce616a8972a9f7a33deda87ea6d4f3c", size = 225766, upload-time = "2026-08-26T14:56:27.455Z" }, + { url = "https://files.pythonhosted.org/packages/96/34/eb8c658f86dfe562ed49a887a27424bfe9e618c26ea6f865b093d075d3a6/websockets-17.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:77b37cceca17291897c3c73bd30a7c7c7909593554b5da574ec852af83c1742a", size = 223323, upload-time = "2026-08-26T14:56:28.807Z" }, + { url = "https://files.pythonhosted.org/packages/1b/7e/2629609652ece5ca0c7ac235927dd4511b08131e3a5d53439b798fddf002/websockets-17.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:d8e83333385cac6030a5167fd18bf96cc6c58b914c308e683f05b0cf94bc8dd0", size = 224276, upload-time = "2026-08-26T14:56:29.991Z" }, + { url = "https://files.pythonhosted.org/packages/a1/6b/8525737fe840b38e5f40956c198fb586a4fac1e07144d41a5b949b989cf8/websockets-17.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:073c5c3f7e127041fa9d34a9e29ceefee8c3cafbd267ed2927318f425144380d", size = 224558, upload-time = "2026-08-26T14:56:31.184Z" }, + { url = "https://files.pythonhosted.org/packages/74/ab/3a958c6cbcf74b118f601c20a80ac8bd5e8dfec0bcf7345116feaeefb121/websockets-17.1-cp313-cp313-win32.whl", hash = "sha256:2afb58c7ba48b329d56769f8dfd89f394efe587b65ef806bae810a484d6d3608", size = 217475, upload-time = "2026-08-26T14:56:32.431Z" }, + { url = "https://files.pythonhosted.org/packages/22/36/fb521f0f2994c25509651f169efe5582dddd8713d57a0757ba87859372ef/websockets-17.1-cp313-cp313-win_amd64.whl", hash = "sha256:0340bbef6bfbe16da888b3983d666a4db4954ac3253c38f13bc7aba0c7db5a2f", size = 217784, upload-time = "2026-08-26T14:56:33.608Z" }, + { url = "https://files.pythonhosted.org/packages/68/92/9b8419584681a12a7534b746dfb2737c466efe2455483e2fbf8b941a04ec/websockets-17.1-cp313-cp313-win_arm64.whl", hash = "sha256:7a72efa3bf4fa3a6669a54420a472ad056da3973d827f10e3a536da463f926c2", size = 217715, upload-time = "2026-08-26T14:56:34.865Z" }, + { url = "https://files.pythonhosted.org/packages/90/0d/500cf5daea09d4669dff3a7d67159094a0bd6c4ef130381404f6edd3eb5f/websockets-17.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:0c9982938980e086da59f70d05f9418cd143401a601a0faac10fa48f7bb1cd3e", size = 217048, upload-time = "2026-08-26T14:56:36.03Z" }, + { url = "https://files.pythonhosted.org/packages/97/12/5b12c6168aa269cffbfd24d177cd492b130120403a418c7e89462e27b4ac/websockets-17.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:57b39dc8541cf7ed3f639da82bf7451060483967f9e733da1f8173e4095f0642", size = 214737, upload-time = "2026-08-26T14:56:37.43Z" }, + { url = "https://files.pythonhosted.org/packages/0c/36/e453e5106e4e2416f008ac222837c2f1637a063b08008afcd1088889b631/websockets-17.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:96abdecbaae746851b87c3a36cb4a661df93ca3d92f114270f79228bf1d00de6", size = 214955, upload-time = "2026-08-26T14:56:38.71Z" }, + { url = "https://files.pythonhosted.org/packages/dd/30/0204bb86176db02cdfc678ce65ed808a66fab87d250ce61a8790800a60b0/websockets-17.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d9fc873e239c5abeb150bc24dbd1a7af23a9254526383ce0a077f5e20adbeb19", size = 224331, upload-time = "2026-08-26T14:56:39.924Z" }, + { url = "https://files.pythonhosted.org/packages/46/c8/d8372256e00c4e3cab1115c45075d1eeedb642a3f2b42bd70c4deae03f06/websockets-17.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6f42912fa9eb4cb7c7ec9fde9b3332ba339eb8a8811981043d4029599f3d950b", size = 224685, upload-time = "2026-08-26T14:56:41.169Z" }, + { url = "https://files.pythonhosted.org/packages/12/7d/650355b8f67f908ff99603351d4458d1a0b787d627950a47c38db7e25308/websockets-17.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f98bf378d7a5be047a044a1a27c987a8f355e10e3b5754617dbe756248cbc5ce", size = 225927, upload-time = "2026-08-26T14:56:42.359Z" }, + { url = "https://files.pythonhosted.org/packages/34/6c/a9ffa5b903579eed76017870f055d75ecc73988d9d0c9b65a92ba0bf2a27/websockets-17.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d334d11398086bb5559606cb42d51c013ea7c061c7db701521392373d3c087f5", size = 227300, upload-time = "2026-08-26T14:56:43.538Z" }, + { url = "https://files.pythonhosted.org/packages/9b/5d/4551c2269066af7481ee44605a0813770961615b5b5da3e87a8f5cb859ea/websockets-17.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5c27336b1a0ac56569493e858497870347854372395f50483725f8cdacc5a45c", size = 226533, upload-time = "2026-08-26T14:56:44.669Z" }, + { url = "https://files.pythonhosted.org/packages/3c/43/237a99233e5c445759a613831b3a92e91905afc064dc3bd0ad33c35fd1e2/websockets-17.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:67258b00302a5aaf0b267771c7014b13429abd7ea17eebc4c55bd935ff101555", size = 225280, upload-time = "2026-08-26T14:56:45.83Z" }, + { url = "https://files.pythonhosted.org/packages/d3/b5/e9407a91613d1d1cd932414143a1012096b26674a782fc55a0bd23217ee4/websockets-17.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:455ffeea0879d313205df1e745e5883e1feb7f31ecd26be882f5f0babd3db04f", size = 222540, upload-time = "2026-08-26T14:56:47.053Z" }, + { url = "https://files.pythonhosted.org/packages/db/d2/db76628db0577b783205d9779f64d8e373416b04c62d1546be4b75dc8540/websockets-17.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f7233eaf441a345a5943a929fd4b5ea3278f11aed35a9ed0f3106b8cb3ca846a", size = 225354, upload-time = "2026-08-26T14:56:48.32Z" }, + { url = "https://files.pythonhosted.org/packages/a9/4c/2174181c067b89a74ae18e2650c2ac29959f4b796afe876ab3f4d30d642c/websockets-17.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:c65da239a5ad553619804c1f9d65c1a0b3005381c6158ee14da2c7444cbd0c78", size = 223867, upload-time = "2026-08-26T14:56:49.579Z" }, + { url = "https://files.pythonhosted.org/packages/df/75/274decb9a8253561b5be3261e02a6676fc8ecdf31e95b722e53d5bfb8fd2/websockets-17.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:9fa1ffa08c81a4f809cdab6129f8e55bee4650b9d6d3461019dda73aacd146b6", size = 224652, upload-time = "2026-08-26T14:56:50.885Z" }, + { url = "https://files.pythonhosted.org/packages/9f/e6/49824f1fb4db7656d2f7492b1d8be16147b759d909490e32f4776843ee64/websockets-17.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:406b8107943a43ef4649b1e0cb0cdc052bbf08fe1c8905a623c4af9586e5cebb", size = 225822, upload-time = "2026-08-26T14:56:52.356Z" }, + { url = "https://files.pythonhosted.org/packages/b8/6a/5dc43838c0b02a95f42c47a0de33c5ddd7767a9feeb4d0d8777ac1cfefe4/websockets-17.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:4e8ffcb486c8490a34a4cef5e4409d8da5a1cb1681e5bf7d786ce5e84aa8540d", size = 223379, upload-time = "2026-08-26T14:56:53.699Z" }, + { url = "https://files.pythonhosted.org/packages/c2/62/585637cf06d6b321232f79c55dc14d65518d12cf87c94c44f5864068810e/websockets-17.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:fb88076df585b69c5761c387c0081aa87d7b9eb1b205a6535ca4777e25650d81", size = 224330, upload-time = "2026-08-26T14:56:55.184Z" }, + { url = "https://files.pythonhosted.org/packages/de/68/c3b234a6a1366b6ab5bbfaa4434a1b946e1dc4e8ddd6824bfd93a8835b7f/websockets-17.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5d4724255fb8398acd9e583b97eb2279cec20e0bd0f9a94bf75f6056ef9f13da", size = 224622, upload-time = "2026-08-26T14:56:56.393Z" }, + { url = "https://files.pythonhosted.org/packages/6a/d4/84cf3d1376f5d8207f55f43c1c818babd6b89447f5dcd01f18a6d5526796/websockets-17.1-cp314-cp314-win32.whl", hash = "sha256:be3f0129c5654517b2abf07dcb75bb1d9479759a4ccfb569e8293579e9fc029a", size = 217036, upload-time = "2026-08-26T14:56:57.652Z" }, + { url = "https://files.pythonhosted.org/packages/d0/0f/9e7ac63c5d7cb642952200814f584318e65146df008b7d375d5d9c6b2c97/websockets-17.1-cp314-cp314-win_amd64.whl", hash = "sha256:2a4dc6ef83f4559e0d05f313a375cb38f63c986096a9da99fe94fdd779d313e5", size = 217382, upload-time = "2026-08-26T14:56:59.065Z" }, + { url = "https://files.pythonhosted.org/packages/54/bb/1ae6b91f7f3ac05f5c9f14a72dc2181c115ff370bcd8a7f10f02c174adfd/websockets-17.1-cp314-cp314-win_arm64.whl", hash = "sha256:46c0331c9eaaf73a559f3a9e388466be0df96eb83d40f06f1ca6ab6613b35c82", size = 217268, upload-time = "2026-08-26T14:57:00.654Z" }, + { url = "https://files.pythonhosted.org/packages/b3/f0/f65644d0e0b2b90918a8c41503841cc4072a58f2bf76c09bc36e751fc0dd/websockets-17.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:d411ea5ca18ac1b12c0c94be88b60c18ca641ac43bcdfdf1c9f79d46cdbe1603", size = 217379, upload-time = "2026-08-26T14:57:02.181Z" }, + { url = "https://files.pythonhosted.org/packages/ff/35/4c46d1f620ac1a30f92b6eae78ee40a772a93f568647ca7ccdc5ea283cf8/websockets-17.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:07fa3e7c30e2c577928d359b56bf872a3e0cbcc15553eaa0907c1ee86344b56f", size = 214911, upload-time = "2026-08-26T14:57:03.478Z" }, + { url = "https://files.pythonhosted.org/packages/04/6e/4587e8406d7c1188e97b9cf466c081e93399380d447f885bfce81626cd37/websockets-17.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6de9acef07e3a78e9567fcd26c29011a4da8f050b13004bbf880a0fd82a6eea5", size = 215115, upload-time = "2026-08-26T14:57:04.692Z" }, + { url = "https://files.pythonhosted.org/packages/ec/06/1381c8fff525041025909eb80ace32489194a00ba22a0a8d428030afcc84/websockets-17.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:ea0ed9373b880115911d9d39634bccc95b8ce590c9c42e8589f5cacc3ef3cee2", size = 224696, upload-time = "2026-08-26T14:57:05.899Z" }, + { url = "https://files.pythonhosted.org/packages/36/9d/9034e867dc85340be058619751742b895f722326e83100d110063461ca07/websockets-17.1-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:50903d335bfda026c2fa11dd9aed09d8cbee0c451e3a85122a9acb041b7dc69b", size = 224975, upload-time = "2026-08-26T14:57:07.262Z" }, + { url = "https://files.pythonhosted.org/packages/40/eb/ed03aa3cae748ebf6397e5d44028f433f746bad09dc568ff754fda3a3c9b/websockets-17.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5a74531ce81af587f906ab42f194032388fcff8fc7938402e5917c9147a39441", size = 226151, upload-time = "2026-08-26T14:57:08.524Z" }, + { url = "https://files.pythonhosted.org/packages/b1/c9/cc1964a096d16f3b73cb1ee5f14f277f5a3bcac07c6e8f9a1dcded99f4c8/websockets-17.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8fbf28e639544503b7d1c96452a5e5e043e4108d89b1f3fa02910603622d19db", size = 228292, upload-time = "2026-08-26T14:57:09.846Z" }, + { url = "https://files.pythonhosted.org/packages/1a/26/46da6dd0363c2db2e4876fd59a40fd40c1943a82d7018d0a33afbce47d52/websockets-17.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f612dc57f00c07cf4aa2673f7cbceabd654ad2457b7e639f061b794d6e11f9fd", size = 226722, upload-time = "2026-08-26T14:57:11.118Z" }, + { url = "https://files.pythonhosted.org/packages/78/98/ecd8f5e1c5d0e54c08ebc5c66852271112166db68107cb0e17ca1bf25009/websockets-17.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1c7ac77401227212dc6e849182feee50d57cf456ec6329ffd6979c94bb136c5c", size = 225451, upload-time = "2026-08-26T14:57:12.601Z" }, + { url = "https://files.pythonhosted.org/packages/65/4d/da8d2760db53e17aae763738b6ba834b1fcf16813d3632f3edb6951e1ec8/websockets-17.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:32a2a68d989d6e5b74a9d5095415c51189ebae29fceb7cf2b64a1c0318a81256", size = 223003, upload-time = "2026-08-26T14:57:13.875Z" }, + { url = "https://files.pythonhosted.org/packages/a4/40/ea401c141a79c5b1d0021a0dab9d0df2051c108f1620fbb39a6e7c714c3b/websockets-17.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:aec00f018d34c67500ff0438dc314b40277be4a1b983cbacbf53ccf7db63e257", size = 225704, upload-time = "2026-08-26T14:57:15.091Z" }, + { url = "https://files.pythonhosted.org/packages/e1/8e/07ab3f44215d89840d5385fdcaaab1fed8caeffa67c6899e15062957c12c/websockets-17.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:0014eaff8ad5b3b43feda2279f9d34bf2eaae040720b9fbbb55944b10f40b14d", size = 224192, upload-time = "2026-08-26T14:57:16.3Z" }, + { url = "https://files.pythonhosted.org/packages/58/93/ccf1af0a23e5748d4e22292a377d78d15cf294d7e707bbb11a8990ae6bd5/websockets-17.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:db9d7ee47f3ba531e278be539af39e2c7c7d28fb94897b6cd1120d63b0ef5922", size = 225082, upload-time = "2026-08-26T14:57:17.531Z" }, + { url = "https://files.pythonhosted.org/packages/e2/db/e32200f99ce282e728d2929f2c429db353cf3282db7d0eba99eb32c9fec1/websockets-17.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:ff3e2ba7a9f0a110b0555452e9b5a03a34e11662544e01beea15f144b48ba7b7", size = 226101, upload-time = "2026-08-26T14:57:18.802Z" }, + { url = "https://files.pythonhosted.org/packages/28/3d/e7a6e9777b29433620167c98f3caaff0d6b08b1239a273ef7f7fd1393349/websockets-17.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:6da17fc94bd270f5987b10bee113461ac36a36a98b0481ddcc98056e5a90001a", size = 223794, upload-time = "2026-08-26T14:57:20.313Z" }, + { url = "https://files.pythonhosted.org/packages/48/05/ac569090726dedd6656f3ee28b0c02dfb1ba76e898dceaccc2987a237cef/websockets-17.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:e8dc3fa6d6b7ead3f9de57895f41b116a28787548e066365d9d90f7356bcaad2", size = 224567, upload-time = "2026-08-26T14:57:21.634Z" }, + { url = "https://files.pythonhosted.org/packages/14/50/4ef62941111db6b31193f4fabbb65f845a5177579040cb8fe0d774d25034/websockets-17.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b65d5fe48219dc2d5e158de9e6514e75600f379cc7e37108d35f31764c155566", size = 224993, upload-time = "2026-08-26T14:57:22.86Z" }, + { url = "https://files.pythonhosted.org/packages/28/42/2b95ada4ea19bf3a2072b68669ce4f4afb212690b727d31640576287fd68/websockets-17.1-cp314-cp314t-win32.whl", hash = "sha256:2cce251f3e2469b99b6802b55435bcdd07123b41870f54c87b336183af9d7e68", size = 217168, upload-time = "2026-08-26T14:57:24.466Z" }, + { url = "https://files.pythonhosted.org/packages/32/0a/67d5ee08dd8060a37d612fd40a625b5376ad19ae48fe1c8ad428c278b817/websockets-17.1-cp314-cp314t-win_amd64.whl", hash = "sha256:8f6c38cdcaf98a911d7acc25577f2f9e710f3a2fc2bde1563556784320196b51", size = 217508, upload-time = "2026-08-26T14:57:25.983Z" }, + { url = "https://files.pythonhosted.org/packages/76/a3/822005d0c674451d2411027b878cdc128a2b7ea5a30d337d9e279da22eba/websockets-17.1-cp314-cp314t-win_arm64.whl", hash = "sha256:d1e2f5fa2b6d01f0d85b4f223fea7ed1d504be282a02a81bd2be4817ef7a2f03", size = 217425, upload-time = "2026-08-26T14:57:27.324Z" }, + { url = "https://files.pythonhosted.org/packages/de/d5/99a6c6a1eb5d5ae9f45f59a3c97f4e3b21f310eb404a547fb3e7d2fc054c/websockets-17.1-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:88381602e379165b66244b2ebc29f9b23ea0851fbe63ae157f91ca324f072d6f", size = 216970, upload-time = "2026-08-26T14:57:28.575Z" }, + { url = "https://files.pythonhosted.org/packages/a6/0e/1e7f6e833728193958d3ed3d67b5d57c3c7cfa948abf94d4bc553257c954/websockets-17.1-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:88bc5138e53903a85c354e59df7ba73ce306f7b09724cef74dba121e60a88ce2", size = 214699, upload-time = "2026-08-26T14:57:29.862Z" }, + { url = "https://files.pythonhosted.org/packages/07/00/95d39549f86e34425a0412bcbe61708dd1fc46af654e2134a6c4389102ad/websockets-17.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:3546ef55b3a074494106508bc6505c73825970d2d9505f7bf53882b3e88b0d1e", size = 214927, upload-time = "2026-08-26T14:57:31.148Z" }, + { url = "https://files.pythonhosted.org/packages/4c/ff/b442415fc4f7f9943b0fc8e8eebaa13923ca73361e167c439ba634eecbd9/websockets-17.1-cp315-cp315-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9ae55d24241fc055f22aea3ac924559069848bd0ad4ea065fdd72d2194685fe8", size = 224373, upload-time = "2026-08-26T14:57:32.833Z" }, + { url = "https://files.pythonhosted.org/packages/a8/dd/b83537aae4cf61615b9d8b2dbb235c0030ba85457a6d934798273814600f/websockets-17.1-cp315-cp315-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d7b349265fad6244013eecd99df8d83c12bf3013943e431f4fadd5bffc37db42", size = 224801, upload-time = "2026-08-26T14:57:34.041Z" }, + { url = "https://files.pythonhosted.org/packages/76/83/5ab0abed58454909e8dbab45086ac68ee4556d7a8ada26735addc909b903/websockets-17.1-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dc5789e5ea182b77a38881383ada5347202a6c66f4857d054e075290e80b604b", size = 225967, upload-time = "2026-08-26T14:57:35.292Z" }, + { url = "https://files.pythonhosted.org/packages/4b/26/e2412f2b998a8c1dfc00c0709ff6ee0c634dd0b0b4f92bdfe9667876b71c/websockets-17.1-cp315-cp315-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:ce13c7d233239e739600a57d4a73c1192ad8259e655a4d55aa1a454242bc809d", size = 227664, upload-time = "2026-08-26T14:57:36.493Z" }, + { url = "https://files.pythonhosted.org/packages/ec/25/0dd4495df3c0e02f6db705312ba85ab9b2dd42257dc23eb0da10066e4844/websockets-17.1-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1036189bd34b0bc1b10a4679321e2c7968af317efe6e8e4c1c5141c4254fb5bb", size = 226447, upload-time = "2026-08-26T14:57:37.781Z" }, + { url = "https://files.pythonhosted.org/packages/be/67/6df3f63ffc48f08126ed0cd2fd2a41092967c3e364f8ec100deae90b6d77/websockets-17.1-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e78fd4b7b2c5086a38671c9c882c1e643385eccea360b5b1fda4a105e590087e", size = 225343, upload-time = "2026-08-26T14:57:39.133Z" }, + { url = "https://files.pythonhosted.org/packages/b1/8d/a8479bbb09ff054907d141123d8f52fb6ae5ac39c6dbe39e6a02a8408309/websockets-17.1-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:46e7a10bf04318c7b0c0273791925ae5e1cbe4a11e34aa934d2ef27862058a80", size = 222748, upload-time = "2026-08-26T14:57:40.478Z" }, + { url = "https://files.pythonhosted.org/packages/40/fb/4c3d2a3269cde3f3087916de9c3d9fc5d7196b46846d8c3a9ae59ad0a884/websockets-17.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:33e45c7ea38428e740a7f233555d71df0b875cef7fc080acebc9654475e35335", size = 225453, upload-time = "2026-08-26T14:57:41.859Z" }, + { url = "https://files.pythonhosted.org/packages/7f/1c/6467b401d19408f34e1c7389c222c2c7e1dfdf08c551190269b5eabc726c/websockets-17.1-cp315-cp315-musllinux_1_2_armv7l.whl", hash = "sha256:6e63c01803be425ff062b7f7fc201a74def1d49fc94a2410dd17375df75936e9", size = 224112, upload-time = "2026-08-26T14:57:43.136Z" }, + { url = "https://files.pythonhosted.org/packages/c5/5f/744e032ac80e11039a7447657ebabb46e9b5c2dbcec83be571335212932f/websockets-17.1-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:722ec21717eec6477bce582147a28acdfe034e604239466a6a95daedb863e774", size = 224646, upload-time = "2026-08-26T14:57:44.871Z" }, + { url = "https://files.pythonhosted.org/packages/9f/47/bcb9128d9afc4d0934d9192e2a24897ca2f7a63df2654904915349c6c46d/websockets-17.1-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:e74e41f0ad12ff1e8983e349daef79d37cc8280c743ce9d134d6c74c18dab5d6", size = 225797, upload-time = "2026-08-26T14:57:46.338Z" }, + { url = "https://files.pythonhosted.org/packages/c7/e0/b058047b7cf565e1105b10ef6b6b24a6ebe3575678c7dc75a645334705a7/websockets-17.1-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:12fe8984a32dbfd084e0603f1a8d740c0180cb85b3174585c54a80d2455a8394", size = 223605, upload-time = "2026-08-26T14:57:48.175Z" }, + { url = "https://files.pythonhosted.org/packages/b9/69/fc1555bff884de363f1bf9eebf2836dbeb29fa7e4f957debb7bbcf43abba/websockets-17.1-cp315-cp315-musllinux_1_2_s390x.whl", hash = "sha256:01dcb47deebc40b38fd4a493b9b9f4d0a704b7bec6f35e4d34085b329abce71a", size = 224508, upload-time = "2026-08-26T14:57:49.407Z" }, + { url = "https://files.pythonhosted.org/packages/e7/f9/648d4e68621688b19093b06f7b497d520952e68cdea1c1b54371fe9491de/websockets-17.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:f4c45ee2512d3757b5e6c67c5a34e435143f2ecb7df3324f9fd888688c45c0f4", size = 224767, upload-time = "2026-08-26T14:57:50.799Z" }, + { url = "https://files.pythonhosted.org/packages/58/93/f8342b55864f71df13eb8e9ef7dce691b87a87f04f75bb8a1385b3336e7c/websockets-17.1-cp315-cp315-win32.whl", hash = "sha256:0f4f50dfe2cc810fc4e2de979b35e83bf8bb4bccdc6fe472d93762ea7b1d5927", size = 217003, upload-time = "2026-08-26T14:57:52.122Z" }, + { url = "https://files.pythonhosted.org/packages/ea/f0/7b5fdb774c245e0b6217009e2a24d2105c1a64923949f33be41aa7959302/websockets-17.1-cp315-cp315-win_amd64.whl", hash = "sha256:4af784f3e436f65b355c117c6497320f2b5cf6a559295cb1c4c7338e335d45cc", size = 217300, upload-time = "2026-08-26T14:57:53.492Z" }, + { url = "https://files.pythonhosted.org/packages/76/33/1fe6ed1b5087516115ca451b2c240314b010647071f8fc3bd78a21e4dddb/websockets-17.1-cp315-cp315-win_arm64.whl", hash = "sha256:d58159af7835fde09c462394293c0d7aaf8fb4557d8f8e5699f5e722ccae013d", size = 217214, upload-time = "2026-08-26T14:57:54.88Z" }, + { url = "https://files.pythonhosted.org/packages/94/ca/ed02e75996a266d76c5fcb5dd9b930db4cf2b388ca5fa3d2a72086f81568/websockets-17.1-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:1a5cf4e7bbe3ca499e6a289206cb4fcb7444b09919e129bd517f57d5fa192c13", size = 217282, upload-time = "2026-08-26T14:57:56.108Z" }, + { url = "https://files.pythonhosted.org/packages/bd/7d/d536f5bc89ea5b52fd1c1727c59fabafee6bc41f5ce92c3bd2f83047908c/websockets-17.1-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:416b4bc8789a1865a3ff643ec4ee073a5f52402d0dbeafd27b1798d5dd6b6a51", size = 214863, upload-time = "2026-08-26T14:57:57.355Z" }, + { url = "https://files.pythonhosted.org/packages/37/37/944cf17bad668e9be1247e6314f88a48b9faf7c250e383410db8b38af0b9/websockets-17.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:259f45358c76d3b18489e3e80636cdbe807e05ecf1b10fdf1a779106d23d0c8e", size = 215073, upload-time = "2026-08-26T14:57:58.719Z" }, + { url = "https://files.pythonhosted.org/packages/74/bf/3267966cc1bbc2b8fa62fd329651b0af502df1f5d1c0eed027ff339d6aa8/websockets-17.1-cp315-cp315t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d9d01e8ede41fea4f5a847dad9d628355f74905f437a5b6856d67aa66d193800", size = 225229, upload-time = "2026-08-26T14:58:00.235Z" }, + { url = "https://files.pythonhosted.org/packages/7f/d8/85ea722f483510abb39fc71aafb4465d17cf9051a275ab036874ff3c300c/websockets-17.1-cp315-cp315t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a7b35181a14cbfcae163b4de545d22abfd07d06c2c41ca69cfcd99251d6888ab", size = 225500, upload-time = "2026-08-26T14:58:01.994Z" }, + { url = "https://files.pythonhosted.org/packages/50/ce/64c7d00005bd0d15ecb5c5fcb7fb2597b6b92ddd16c4fa6bbc3d2835ad63/websockets-17.1-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6a8e768a048c2220697477ce2e67e4345dc9f693d0ee6af53945b5e30227c6a7", size = 226829, upload-time = "2026-08-26T14:58:03.327Z" }, + { url = "https://files.pythonhosted.org/packages/b4/dc/096c67940fb957e667ca3c542818150434eb0388c6fdc90b3a502f3c3e96/websockets-17.1-cp315-cp315t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:880069d21cc33a558dcf180924a546d1ecf8ada5be3e4e70acee87019d706a24", size = 228457, upload-time = "2026-08-26T14:58:04.78Z" }, + { url = "https://files.pythonhosted.org/packages/51/fe/f2331b6b7ccc67589891da354fa46a5cb79e95f83b9fd0e734d77f1f2140/websockets-17.1-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cec1bb8f22abccc8d20f8ca63df9be41600c26c190f4b97ee86c675fd4a863a6", size = 227265, upload-time = "2026-08-26T14:58:06.102Z" }, + { url = "https://files.pythonhosted.org/packages/47/a5/fb1642302f8ec77ca922203074f155a9831a5128ad75e725059a476d1227/websockets-17.1-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f3a1d577e081667dda7f8e5b4796e6e32f9713c93e2a3d930669519840a3c623", size = 226143, upload-time = "2026-08-26T14:58:07.464Z" }, + { url = "https://files.pythonhosted.org/packages/d7/41/7133fcfb63f5562750b269d6a845c689dde6a2c6407286da395beea19ddd/websockets-17.1-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:dc053f9e95a76213c5eb7ed95779f7daf0d2bf0e4e03073629ebfa43a033f151", size = 223501, upload-time = "2026-08-26T14:58:08.766Z" }, + { url = "https://files.pythonhosted.org/packages/64/b1/82b36bfabc79ff2d383a1fc043cee6a13f794ef4f6bf1b4810ad6988cf6f/websockets-17.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:bb0efe019480a1c93e168ce96479273aaebd672fc8c350d5eed1e507ababb1b8", size = 226330, upload-time = "2026-08-26T14:58:09.987Z" }, + { url = "https://files.pythonhosted.org/packages/41/7d/5b511b9bf6e9ad331e6ff902fcbcc71c3794d10ef3b5efe80ccb8f0a7861/websockets-17.1-cp315-cp315t-musllinux_1_2_armv7l.whl", hash = "sha256:615746b12b26a3fd4077bc6fbeb277a1c192a45dd57b531d07ad9ed5c52a9a7a", size = 224980, upload-time = "2026-08-26T14:58:11.303Z" }, + { url = "https://files.pythonhosted.org/packages/e0/50/aed08f25301f8eef23be903ff9319fcf35630ca2bdec9d226f7d804dd5b3/websockets-17.1-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:1a20136d61f9ca3a31493732762661fafc2c20e8861930214e21afc6a8a692a2", size = 225478, upload-time = "2026-08-26T14:58:12.543Z" }, + { url = "https://files.pythonhosted.org/packages/3e/47/0d63d4168536b4682c9d19b7399443b1176f25dbb68878374fa716670230/websockets-17.1-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:2786cbd273ab69c22612db8a41229ddf2c158060b17b5928884bf388d07887f3", size = 226588, upload-time = "2026-08-26T14:58:14.457Z" }, + { url = "https://files.pythonhosted.org/packages/b3/dd/844bd0b6386fc81ed6a55f4b6dd26f01c6987eda205afa10175ea12b2164/websockets-17.1-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:b1c323fc3be1dc3f87f6c59458cb7d9e13dcbbf971d6c3f3e2bbaf58d3bfcdfe", size = 224336, upload-time = "2026-08-26T14:58:15.778Z" }, + { url = "https://files.pythonhosted.org/packages/96/18/03709c84bc88ec4dcea68d4be4ccd07d611073dec111203a5bf45af8809d/websockets-17.1-cp315-cp315t-musllinux_1_2_s390x.whl", hash = "sha256:12c8e2b25df59755954a04dfa09c990b96691025aaf7eafd19ed6da24b09c18d", size = 225197, upload-time = "2026-08-26T14:58:17.141Z" }, + { url = "https://files.pythonhosted.org/packages/27/cf/0d1c694b6466c89e875b85b32b51312c472cf6708eee91914866f5087dde/websockets-17.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:f58f58b4b29bbea2a3635e2c56eff4a3adab011fe383802a9e542e31b97085fc", size = 225493, upload-time = "2026-08-26T14:58:18.521Z" }, + { url = "https://files.pythonhosted.org/packages/4e/f5/99857c3dd9676749f33e3668665a34ad6099505fb8d75eb084f49f7807a9/websockets-17.1-cp315-cp315t-win32.whl", hash = "sha256:f78a3ffb1994304db2c0c4588e4d1a518079b557054fa3bb985a6f5e50ff49a3", size = 217130, upload-time = "2026-08-26T14:58:20.037Z" }, + { url = "https://files.pythonhosted.org/packages/2c/84/77599922ab441bfe61508f97dab2c71f8e114d31793993ea54011db16199/websockets-17.1-cp315-cp315t-win_amd64.whl", hash = "sha256:ad68c28a27246fed109a4409393d677b7e1388345cbbd2f5aee5c182d8506110", size = 217448, upload-time = "2026-08-26T14:58:21.382Z" }, + { url = "https://files.pythonhosted.org/packages/ce/3c/8b9a225b523f06a9389be81f1b0ab07c49bec6014742e6aa359c1f920f1f/websockets-17.1-cp315-cp315t-win_arm64.whl", hash = "sha256:e552e0037230ac16e5f568de7012041344d1b18c9feed30ec2891b8eba55af81", size = 217372, upload-time = "2026-08-26T14:58:22.807Z" }, + { url = "https://files.pythonhosted.org/packages/e7/e4/af4abbcf07eac6a725ec6f865611526b2b0c23d482723de551bec667880d/websockets-17.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:10ecb38ffc05e1841b619d99c725307a223ef9ad58e7b1ed33311d472dc43918", size = 214602, upload-time = "2026-08-26T14:58:25.211Z" }, + { url = "https://files.pythonhosted.org/packages/4d/fe/819fba7ba35f92b639333da7355041c07dd50048f9c76fba0b8e292a6483/websockets-17.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:17aa424ab61620aad21b36b2240efc87b500cc496e7d0e999a5c2ae99395e886", size = 214874, upload-time = "2026-08-26T14:58:26.689Z" }, + { url = "https://files.pythonhosted.org/packages/4f/a7/d370ab794f47fbeea648d17ad08caf0bb50131d6c04b7ad83e6af63c405a/websockets-17.1-pp311-pypy311_pp73-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:764cf7bfa149365f32b7a0fd9fed32debdac29dd06295d5635cde1745b446cd8", size = 215821, upload-time = "2026-08-26T17:25:23.616Z" }, + { url = "https://files.pythonhosted.org/packages/9b/6b/251b00fe634e2a9c2cb5d6390e0e97cec55e3d18dd09b4b976620eed5d7b/websockets-17.1-pp311-pypy311_pp73-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8d1b108bd8f5f6a8b90801f6db3b3858d5deca889acfdb8ac497bbb24e4b0edf", size = 215714, upload-time = "2026-08-26T17:25:26.295Z" }, + { url = "https://files.pythonhosted.org/packages/c4/b1/37fe0c96c206b4208a072c3a74add6a72af4b8228be3f5435163c5a6d099/websockets-17.1-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a62d8c424383c9dc769ff3672018df822603117e32686e567d452ed035b6fb2e", size = 216608, upload-time = "2026-08-26T17:25:28.134Z" }, + { url = "https://files.pythonhosted.org/packages/be/7e/75a0a491b512412e08333b9f8412757af6186fe1c598186261002de1a793/websockets-17.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:8196d217eeca52b9235ee1f8a684a09885a5f953d5a31e80ef915bf2c5c94f9d", size = 217870, upload-time = "2026-08-26T17:25:29.745Z" }, + { url = "https://files.pythonhosted.org/packages/41/63/23572870e01836a98346075b9e17a8bc24a6ddd9800a3204ceee58677f3c/websockets-17.1-py3-none-any.whl", hash = "sha256:f221081107b8c48184d99f7019604486376e7ef826037e70aad6b02540732c23", size = 211134, upload-time = "2026-08-26T17:25:31.397Z" }, +] + +[[package]] +name = "wheel" +version = "0.48.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d0/20/50ed6bdf27dec98b568a8ae25dc599f35baa3d9709f9e83fd1edb56b9a90/wheel-0.48.0.tar.gz", hash = "sha256:94800765601e9171bf5d58d066e640662842bcedcbab982b2c90787a2c987322", size = 66471, upload-time = "2026-08-11T22:02:27.327Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2e/29/69cfbb602cd91690c55d38ba9fe53e6a7e76a6fa647bf38f19c138d25449/wheel-0.48.0-py3-none-any.whl", hash = "sha256:3217dcc807155e45db462d7ef2431f5ddda0d7273b700d05a67b271ceb1287ab", size = 33320, upload-time = "2026-08-11T22:02:26.1Z" }, +] + +[[package]] +name = "yarl" +version = "1.25.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "multidict" }, + { name = "propcache" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/75/16/e8be8e2fb175bbf41a0680381a319f1199fae256588241a2ac8677eafb49/yarl-1.25.1.tar.gz", hash = "sha256:03dd38de09bc213e9a8b29761eec33ee1d5318dac0e49d8af36e4d27830e23a7", size = 246245, upload-time = "2026-09-15T19:35:02.264Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/b3/2cea721d495ca57f8f414aa4867ee263f486b274e07463158cf52f02ba7f/yarl-1.25.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9d693bf4bf534e9ba3ae2780cfd577f5135629f7b5ac653490859d0b77864865", size = 143794, upload-time = "2026-09-15T19:30:26.946Z" }, + { url = "https://files.pythonhosted.org/packages/55/e6/cd145cff8e5cf60b8b3c41fbecfa2a45028a8dec3fbc52bec03595ff3d3b/yarl-1.25.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ab2054c5531af2a9ba7b69b8ec91e4f884420e83a8c5e579b013084cb57e5e5d", size = 103993, upload-time = "2026-09-15T19:30:28.661Z" }, + { url = "https://files.pythonhosted.org/packages/ce/7c/fbb40fe2d53747c40aa36a9e2bd2178a202f942bda0b670f3306d4aefbce/yarl-1.25.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:564fdc7085d2245ab84f88882fdb1d6ac0723124bff6ded35bfb1c00f812630d", size = 104010, upload-time = "2026-09-15T19:30:31.069Z" }, + { url = "https://files.pythonhosted.org/packages/f0/0b/5a516f70641092283f57cf3670bdb75e7327bcc0dcb5038697e4dfbfd569/yarl-1.25.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:acae6b45d1ace09b6ba3876da43b88366ef368f73b988c7f57e14231753d4420", size = 116444, upload-time = "2026-09-15T19:30:32.894Z" }, + { url = "https://files.pythonhosted.org/packages/b6/8a/d1a627f827b0a404ae0f5647cab0534959081cde13b0756a783469fb3b5e/yarl-1.25.1-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:1fb2a01ba8cd9c5d2c5dc1ec35e0fc951d04b4f037541d4ac090c993ce58b3d7", size = 107565, upload-time = "2026-09-15T19:30:35.188Z" }, + { url = "https://files.pythonhosted.org/packages/aa/cf/c8e0aaec886840a6c4480fe44eaec7cd4319f79a563b79321473be79c56f/yarl-1.25.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e92b6bcc741b86d67606c40d3cb9c7cc8e6c737f81e31f4a94efc204456c92e3", size = 125006, upload-time = "2026-09-15T19:30:37.1Z" }, + { url = "https://files.pythonhosted.org/packages/50/26/0cce366d54a93cdc8342965dc7663385e4db161625cc1e8b18e786753d24/yarl-1.25.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:72c34ac7ad4314c19362d5ce27626dcc8429bd30bbf8c179f4234078851f9492", size = 128717, upload-time = "2026-09-15T19:30:38.904Z" }, + { url = "https://files.pythonhosted.org/packages/95/0c/a71501bbc1a674ff72c4d6c2b75f4d9a5af819f5244c3a7558080a8802c5/yarl-1.25.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d5add7b4ca7afeea91d52e4d4e4db3b1fe9885b71f07054560d8c4296b7441a2", size = 117728, upload-time = "2026-09-15T19:30:40.809Z" }, + { url = "https://files.pythonhosted.org/packages/e7/6f/c3267ca01defeed9ed9c4ff9b17bd54915432c405945233265b707475d1c/yarl-1.25.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:def538065f9e4d4cf1ae164bd59aba00dfa84f03923e0de4c3788f252d6bcd17", size = 116224, upload-time = "2026-09-15T19:30:42.818Z" }, + { url = "https://files.pythonhosted.org/packages/8f/69/fad57ee52d648431718ee0f1f68966a99c1352c3924688ffbdcc9d3fe51a/yarl-1.25.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0a191bfdb30a79b98e5d175d75285f9fcb78bf0e46ba5efda042e1c72071a0de", size = 116299, upload-time = "2026-09-15T19:30:44.966Z" }, + { url = "https://files.pythonhosted.org/packages/2a/d4/6a8c1e29f33338687ca278cba0a8fbf6525a322c2c02a9a500ccbe041152/yarl-1.25.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:71f42c5b9a948c113bbdebfa544598321431d064ff959d32e99b1feb61d68345", size = 108625, upload-time = "2026-09-15T19:30:46.874Z" }, + { url = "https://files.pythonhosted.org/packages/e8/d2/3a35ae791c9cb6522c106923ff25c3230d999091e5e65511bca23bbd9914/yarl-1.25.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:72849d892954be4d09e569b8b831ac39ce58417fedc767d4308a0fe542018a40", size = 124515, upload-time = "2026-09-15T19:30:49.082Z" }, + { url = "https://files.pythonhosted.org/packages/be/fd/2b022109a6b4af0f7dc371cf7500af380b0d4f034010243e1b0ce218dc93/yarl-1.25.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:efb01a106f971cb3752856bca2318bbdf7f01bd8823779c461586cbe5ffd5258", size = 115711, upload-time = "2026-09-15T19:30:51.208Z" }, + { url = "https://files.pythonhosted.org/packages/18/59/f7586271136c3ddb0126bbfe661844699369b76b555fe38c4efe86870b2e/yarl-1.25.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:a1daf47cd95a7c3a63456336bc5aaa8c86dd3a47d07ed3d0e76132ae4666a5a1", size = 122751, upload-time = "2026-09-15T19:30:53.535Z" }, + { url = "https://files.pythonhosted.org/packages/a0/0b/07f7a2d881f7e16c385b47fc1753382600847cad305dcc7fa0c25828acf8/yarl-1.25.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9489e6abf47ba37f332075a91444c7cfedb03e6ce99fbb2f116bfe1ce810da3b", size = 117983, upload-time = "2026-09-15T19:30:55.277Z" }, + { url = "https://files.pythonhosted.org/packages/aa/9d/8cdceec66a9b940700cb45931741403f045b162afd79bcb93c41cadd0972/yarl-1.25.1-cp311-cp311-win_amd64.whl", hash = "sha256:d7306dee25b8a0e737363f347362b875094b4dc4e367311470656ae420fdbf8e", size = 102894, upload-time = "2026-09-15T19:30:57.606Z" }, + { url = "https://files.pythonhosted.org/packages/17/f1/7ec357db1d3ad2863542d71e8fe64a126bbec17b134cd7d30951196809a5/yarl-1.25.1-cp311-cp311-win_arm64.whl", hash = "sha256:abb1384477f5901d436b5d2e5465954de46ea6098f59163d243660b5c4461d35", size = 98609, upload-time = "2026-09-15T19:30:59.705Z" }, + { url = "https://files.pythonhosted.org/packages/75/b3/cd32ac66ae622b854c2df0ac52106dda220d361b65a64fde7d5b3684aa3f/yarl-1.25.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:94d7aa6debf92a1dd14cb5280b083a764169a13cfb23a452111160274ed989f4", size = 144798, upload-time = "2026-09-15T19:31:01.821Z" }, + { url = "https://files.pythonhosted.org/packages/61/fb/a2c52a8007c2051ba74662afb112ecf3d00346af4c25e33df9d80fd14fb8/yarl-1.25.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:83d4a37e4b95da4d8bda930d6d35b75b4cdadbacbb4980cae290ea3100b5d51d", size = 104583, upload-time = "2026-09-15T19:31:04.05Z" }, + { url = "https://files.pythonhosted.org/packages/be/dd/ee38aec8e09fdf957e50d4085453fbe202f56c6c3b4cf07b81cdb4f09ee9/yarl-1.25.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e029648f9c951db30e98a7d7ec90835db88ec4b32820efe2a9bdc2287e032eb6", size = 104325, upload-time = "2026-09-15T19:31:06.338Z" }, + { url = "https://files.pythonhosted.org/packages/1e/b3/058dbfb1857b484c9cf9cc135659f50b85ce66e03c99e44dc2f7b6161f55/yarl-1.25.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4d781294bb815ecb5ea57ff6bbf8038e0a31a95fdf3e1788f66e0dc100d64b58", size = 115358, upload-time = "2026-09-15T19:31:08.593Z" }, + { url = "https://files.pythonhosted.org/packages/db/39/29693446cf0cf6b15a0e2f75a5d40f93c56819b05b0622196f45e95b5cc0/yarl-1.25.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e12c538e00e7c1b286a07061046b90e8124e6a9793efae2c70db6a4aad07faad", size = 107658, upload-time = "2026-09-15T19:31:10.802Z" }, + { url = "https://files.pythonhosted.org/packages/86/b3/3c4dd7e1af43b931fba95e0a722737f2ea94a6d199c802585282831d7abd/yarl-1.25.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7e4de3ac4adbad3d0bc7c6f4360a7dbff5de2f15e3b723be3198074e17fd9c40", size = 122660, upload-time = "2026-09-15T19:31:12.84Z" }, + { url = "https://files.pythonhosted.org/packages/bd/b5/1b60dbc3cfc9c5712b15148c206748f2bc93953ffdbe25ea75b63dfc89c9/yarl-1.25.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:419f392a1da624877975709e3864dfe833af6cc7671b39318086d456e288380c", size = 126506, upload-time = "2026-09-15T19:31:15.088Z" }, + { url = "https://files.pythonhosted.org/packages/bc/7b/ca212cbe170ac8b96e45317ecbcf9c3c3ecf0cdec98d5b088a9c4088929b/yarl-1.25.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c6f117789d22dce188e5754e8bc65b7e6ebf8cb73963b9fa761f672a5883769d", size = 117050, upload-time = "2026-09-15T19:31:17.241Z" }, + { url = "https://files.pythonhosted.org/packages/cb/c3/72b4938cdbe619ad71ac156182faef4908846b84dc3ca4dbb4c4e6f84014/yarl-1.25.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:80e47012e730da131c9f059c80936783f9659aae22dc31c03c0595590d11ed54", size = 114174, upload-time = "2026-09-15T19:31:19.294Z" }, + { url = "https://files.pythonhosted.org/packages/e8/43/268717870f9ba0cc9701a95181587f6dc8c5f387aab4aeecc83158f38a79/yarl-1.25.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e80f557716fd765439577131e526b8942ffc2c07bdbc5e39fa62f660ba1e963f", size = 114944, upload-time = "2026-09-15T19:31:21.414Z" }, + { url = "https://files.pythonhosted.org/packages/da/84/baa5bf504d51fe062c4bcaf62936da97fffb43285978d0b39984824231fd/yarl-1.25.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:f61964f235a43738bfac50da46fc4254943a7eea3051aeb0b6fc7c992c29fadc", size = 108263, upload-time = "2026-09-15T19:31:23.388Z" }, + { url = "https://files.pythonhosted.org/packages/a4/28/779a2ed9e0152a601a27039bed9aead3f0b79797a67e2c44bfa444622dd8/yarl-1.25.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:e546fe1d4a93ebc2910f0d768baff19faa09843ab3f2036a67ed6e69fae4419d", size = 122184, upload-time = "2026-09-15T19:31:25.343Z" }, + { url = "https://files.pythonhosted.org/packages/f8/1f/118e9e5b8f07694d63fd3222e801d7782270003f1a222aa798df3f8d5933/yarl-1.25.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:cce0727fd5ac04d372fa9bbfde9febc2bcf209aadfcf0468e45dec72719895d1", size = 114001, upload-time = "2026-09-15T19:31:27.465Z" }, + { url = "https://files.pythonhosted.org/packages/0f/ae/a4cf1cf372313734b17996d4007f9f73596e7a178b9485802e5494ecf484/yarl-1.25.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:af4ea5b37403ef4e30f3927eaed540db942bde01d8d3ff083527c0704d1c9c68", size = 120565, upload-time = "2026-09-15T19:31:29.47Z" }, + { url = "https://files.pythonhosted.org/packages/05/79/ad94f93ca731bc9e44d321833ab96b82a4f9f5f63cf773f81a4aeea5ecc1/yarl-1.25.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:68782fdb4027b8d1eee25ec35e9a6db05e863b899eb0310b3a33b6c3fef55707", size = 117060, upload-time = "2026-09-15T19:31:31.367Z" }, + { url = "https://files.pythonhosted.org/packages/bb/cc/51a7b4abf4ac593b8e7eb3794b28e5a35ae26eed8bc04787628d215af82f/yarl-1.25.1-cp312-cp312-win_amd64.whl", hash = "sha256:7d575b54cb3863ef9bc290ea4b009999d55dc237326131e4853cf33e888fee03", size = 102593, upload-time = "2026-09-15T19:31:33.329Z" }, + { url = "https://files.pythonhosted.org/packages/9d/21/0941a6b93a58b59a1ec75e5333bf06929b671309c43c0cd201c172d9c39f/yarl-1.25.1-cp312-cp312-win_arm64.whl", hash = "sha256:bc3ac7bf569f6b64dad04dd7808c7872dae8a97df657856eac05e9b7e3614a85", size = 97697, upload-time = "2026-09-15T19:31:35.855Z" }, + { url = "https://files.pythonhosted.org/packages/7b/ed/2f3129bbcc9a5c8ba12cc2b29d8060a3bab9c8043c456cfd4b5ca3188890/yarl-1.25.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:25868beca8b6765f8f7d0e11fe6dd7c66dd4b0793b9500286d20cc92352126a5", size = 143623, upload-time = "2026-09-15T19:31:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/17/e1/f1bc3390fdca352826676b531d0712736f156919090206700421d46b2c37/yarl-1.25.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:10b2fd95332f0d716d5eee3c9fb2ce8eada19082de7fee83d32e37992fd75c26", size = 104011, upload-time = "2026-09-15T19:31:40.25Z" }, + { url = "https://files.pythonhosted.org/packages/a8/aa/50acc5c3e5da04172ae3c281c75405af4d2ca911e16120ab0563f4dffb66/yarl-1.25.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0f12afda4eea8c8994a76d4df1875c765194f5fbe8a9d197929ea303caee29ec", size = 103677, upload-time = "2026-09-15T19:31:42.46Z" }, + { url = "https://files.pythonhosted.org/packages/30/d2/7d1e0ab9f8390e1fbcede5a6dbf70d23c96ad09b8c5567f3a514d1ddb0e2/yarl-1.25.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:14b79a30a93a3ce2e8832603fd0ab780ada281b0ba5110b519a634f2d7d7d1fc", size = 115392, upload-time = "2026-09-15T19:31:44.371Z" }, + { url = "https://files.pythonhosted.org/packages/71/e1/5ba1e3a2a22139213655e760919038e8ed7e2d4a99826d0bbddb3beb96e5/yarl-1.25.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4bd6340d20ae2c7ca719b87b426e808e90743b676d05d4c26c4fb5ca71f41184", size = 107493, upload-time = "2026-09-15T19:31:46.273Z" }, + { url = "https://files.pythonhosted.org/packages/f5/53/780653d5e0f73831f467cf13548912e5eec97f21dc49fc8daf21da027df4/yarl-1.25.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:126a2533570c554719ca40a1288fdee1700b6bc82e7131aa69fa85252d92e651", size = 122537, upload-time = "2026-09-15T19:31:48.654Z" }, + { url = "https://files.pythonhosted.org/packages/03/92/d54fa70236c6036271c9c9c09fd978df5cbe3ef49ef6c46e9b833476d215/yarl-1.25.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a3faadac7d812ddac258feb57b9846b60c1b437c4f4b9ad42595c6f6fe4390df", size = 126170, upload-time = "2026-09-15T19:31:50.872Z" }, + { url = "https://files.pythonhosted.org/packages/0e/b7/a82a49bf88340b837ef6972b508a1604ae377b9e6904b46b10cf5f1cf925/yarl-1.25.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:be80550d9bfe83d9b62398a37081a90434e6df2d978ec345c3d2820de6beddab", size = 117012, upload-time = "2026-09-15T19:31:53.189Z" }, + { url = "https://files.pythonhosted.org/packages/ef/78/5d684b411e3f3602464ee9b538db48205038f8605872985f61efb809ced0/yarl-1.25.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e07595c7d6f4db270ceede356a1bd1c07a34f1c26f958d1ed0cd7b48e0d2bba3", size = 114950, upload-time = "2026-09-15T19:31:55.694Z" }, + { url = "https://files.pythonhosted.org/packages/2f/11/51d82b852c64f7fad0fc7a7ff3031517204887e874c722bbca839c0b23ac/yarl-1.25.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:eb96ed1ae6c7d072d60840c0434aef07a2df611812810807fbc54263a6053e9a", size = 115428, upload-time = "2026-09-15T19:31:57.966Z" }, + { url = "https://files.pythonhosted.org/packages/e4/49/9d1978049bf646b9ea918313926453c6901b71c92f097467777d47d36a88/yarl-1.25.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:3feb99222553a8cbedfa52c2f59dd84c3f50d5b582c728d522caf8d72769a54b", size = 108428, upload-time = "2026-09-15T19:32:00.048Z" }, + { url = "https://files.pythonhosted.org/packages/43/35/7b8f1ebb45d7ec3dda7d1909bf44f458de41ef91e2937f107733582a5166/yarl-1.25.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:a2ed0ba415ccdf08f14bf544cb78346d0f76086707ffee24921a2c84dbf1305a", size = 121961, upload-time = "2026-09-15T19:32:02.436Z" }, + { url = "https://files.pythonhosted.org/packages/63/d6/d8b689ab7ca26edeb85f6ff28812aac7a25376eefc1780e303a7bfbaceff/yarl-1.25.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2b49375d22299b0a834c2bca72f39aaecc270d96fb24c30424899676f487b22a", size = 114961, upload-time = "2026-09-15T19:32:04.456Z" }, + { url = "https://files.pythonhosted.org/packages/cf/d5/1a1798ea4dc6b7ee3260010a27907ebc697c95dae99817d817ed446d24aa/yarl-1.25.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:ef74070ac553c59eb4f04258722066d6c6135b7baa03b2e9f2da65c096e96d98", size = 120036, upload-time = "2026-09-15T19:32:06.5Z" }, + { url = "https://files.pythonhosted.org/packages/91/8d/b1b35ed7903da6669b1d367cb2c09436acd4ff508029b4f39a0c0c2058fc/yarl-1.25.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0a66db89ea473abeac4b70523cafd94db3772380e565f9d28af7a179b7af71fa", size = 117276, upload-time = "2026-09-15T19:32:09.401Z" }, + { url = "https://files.pythonhosted.org/packages/3a/8f/4db01cef62caff0d7a4593ed694fb8a41a27a11158cab80d290221f13e57/yarl-1.25.1-cp313-cp313-win_amd64.whl", hash = "sha256:1f51020b2eb8a003c84925638ec63c21a750a4bddd3a22ec8eac6a742dadf1b9", size = 101945, upload-time = "2026-09-15T19:32:11.545Z" }, + { url = "https://files.pythonhosted.org/packages/c0/5e/3ce00497c5c0babb74d4130c10c3828ccd215b4819d12020c42429f991ac/yarl-1.25.1-cp313-cp313-win_arm64.whl", hash = "sha256:b10dd0557ba422715b5206b3743192135a6022acca8baec51aa127d0a75db8fe", size = 97270, upload-time = "2026-09-15T19:32:14.127Z" }, + { url = "https://files.pythonhosted.org/packages/80/cf/54023edfab7aa773b860503db0c56e962ccab0922803ee97988c176ea090/yarl-1.25.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:a9ca696eb02e5c02a8afd872ada510eba9b7fe6e68b9572c2e9a9b1941e31e2e", size = 143975, upload-time = "2026-09-15T19:32:16.416Z" }, + { url = "https://files.pythonhosted.org/packages/d7/a8/e6c1be0e6761d0f2d10bbf33a3e1e02b99dc83874d92945d7b461a72481e/yarl-1.25.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:a5877f2255aab518ebe528289037699201d5dc5f045f2396cb30aa02db22f57f", size = 104018, upload-time = "2026-09-15T19:32:18.364Z" }, + { url = "https://files.pythonhosted.org/packages/6e/bb/dda344765ffd3430afe1a1c66c866a57fae67786537d4f14607df6505ac1/yarl-1.25.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7a5c3115595995779ee21f2567035793911c3802a43c74f3fbb0314929ec67ac", size = 104156, upload-time = "2026-09-15T19:32:20.459Z" }, + { url = "https://files.pythonhosted.org/packages/e5/5f/ed1538bcd06009fe990d6d283dd7667f639e62a81e35c6d8c6ef6c08fb3c/yarl-1.25.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:77e5099b99b37f3cf79c246998ca9f7313a78054cd1809ec46bc1afad47e1c4c", size = 116025, upload-time = "2026-09-15T19:32:22.766Z" }, + { url = "https://files.pythonhosted.org/packages/a2/af/2185daf56b99830d3356ecfada46faaa49945de6626e842b7728088d4980/yarl-1.25.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6efaf45df6a849cef613a03a94c845647456662f85438c886bb67a9c027c8c2c", size = 106985, upload-time = "2026-09-15T19:32:24.749Z" }, + { url = "https://files.pythonhosted.org/packages/c1/65/bc1ae564fb4b04a30b6a8f250e787772581c57e4c3d5cf07ac3359de3103/yarl-1.25.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d5f90e44653c4e0f78501ed9bb7d3fce835a8d62b7c6ed0cb16557534087e743", size = 123030, upload-time = "2026-09-15T19:32:27.084Z" }, + { url = "https://files.pythonhosted.org/packages/6a/3e/e2afcde10d74e53b3fa889960991efb3019beda2b1682a01de720a302056/yarl-1.25.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:632da579b2d879f6bad20f2cfa35ded1efe2f4f77f8abb26a6234a5b236acd2f", size = 126765, upload-time = "2026-09-15T19:32:29.332Z" }, + { url = "https://files.pythonhosted.org/packages/a2/be/415b00c0fe5a0615b062a456b26623d7ec91c2bee20faea1a14045aa0469/yarl-1.25.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:30eec96e8a91bd588ce897c9543f6d5d8d34b28fbcba28a4dedf20ebeae9fe57", size = 117199, upload-time = "2026-09-15T19:32:31.49Z" }, + { url = "https://files.pythonhosted.org/packages/97/27/3d8c63ddd3e8bcfd033748ab93876678ce59bacd66e4cb1ed851c9c5b37e/yarl-1.25.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:12b6bc4906e11f5e1a1cdcb12296e7afbd366c783cc8073403cd2fb74334e453", size = 115187, upload-time = "2026-09-15T19:32:34.137Z" }, + { url = "https://files.pythonhosted.org/packages/39/b7/7a81d0be1a502a26a0d4326c6f2ecb736c824f570ea1c6529f2b0b227b50/yarl-1.25.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9d6ed3d17bccce4c05343e1ca8da13bc5c02c812a4e7282ddd05e8769322d3fc", size = 116085, upload-time = "2026-09-15T19:32:36.438Z" }, + { url = "https://files.pythonhosted.org/packages/f0/69/39fff459916aa0fab42215dc47b759586fd80f94aa56dfc4a7c15ba6e0dc/yarl-1.25.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:f38a70074041d3b7e138e452799f5174198bae5bd5ab2000917badf403908c5f", size = 107996, upload-time = "2026-09-15T19:32:38.959Z" }, + { url = "https://files.pythonhosted.org/packages/c0/39/80b9a55a3335590451d9ecf3eb593a8c635351f4c905ef056d7e8a8fd9e7/yarl-1.25.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:4ca89e4e21854ed27ec753297dde84b16c9f8e53b14a4866fb44457d643c19f8", size = 122549, upload-time = "2026-09-15T19:32:41.151Z" }, + { url = "https://files.pythonhosted.org/packages/42/7d/a179c6757818bb59372a4adafd09f7f26a3b4a0f04c3ae404b544c0b0c82/yarl-1.25.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:1ab7618921a93767387a4b83776f751588f5b5ae9bb5bc96620e2e2e00bca868", size = 115107, upload-time = "2026-09-15T19:32:43.072Z" }, + { url = "https://files.pythonhosted.org/packages/32/2b/a773ac867e4ab53a98ed98e5cefe3bae31e6f550252ca9d1de266f1a40c5/yarl-1.25.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:0ae12ff2b805fa02c4dab838005caef735e39986322698c48588d3beacb65c62", size = 120666, upload-time = "2026-09-15T19:32:45.061Z" }, + { url = "https://files.pythonhosted.org/packages/bc/41/52be6505e85b0f76b4f85b01b5de7e06a0512201abc2c95e14e099549174/yarl-1.25.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:90c30ed53546da833c700115c0064c22120d1b1560f474699fd31f22dd668233", size = 117505, upload-time = "2026-09-15T19:32:47.177Z" }, + { url = "https://files.pythonhosted.org/packages/f5/01/349c0386caedbbe488d519f252df54efac8a1459282d466c474bdd84a620/yarl-1.25.1-cp314-cp314-win_amd64.whl", hash = "sha256:acfa7e22aa6c6e7a5996a41d275bfa01efa7ea56ab890590280e9063e2cf5c1b", size = 103446, upload-time = "2026-09-15T19:32:49.615Z" }, + { url = "https://files.pythonhosted.org/packages/5c/f0/8ec63180f77912f0dc4e5a42760cb8c08d20da1d5ace3578a01b84d1f3d8/yarl-1.25.1-cp314-cp314-win_arm64.whl", hash = "sha256:8e7d98cdbb6d71e726f7d525952867096053d1f290dd4e3c50d7d313a136f414", size = 99159, upload-time = "2026-09-15T19:32:51.686Z" }, + { url = "https://files.pythonhosted.org/packages/47/7d/92d2220d6886b70ab1ed8579533ac2af2dfac716d5d929001daff7986df9/yarl-1.25.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:d21f0fa80a02d05299207eeaafef345d812ace96d5306e4ef265e1d419a615fa", size = 150071, upload-time = "2026-09-15T19:32:53.911Z" }, + { url = "https://files.pythonhosted.org/packages/64/fc/b245e448124bcda9340df38e3553fa222b50260fca027a84095e9bd8642d/yarl-1.25.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:17c9877a89fb6e2bca6f9087eb24cd7fb434653946ef5075e470d23d49b52287", size = 106780, upload-time = "2026-09-15T19:32:56.443Z" }, + { url = "https://files.pythonhosted.org/packages/51/e2/9a6ce2e334ebf218a30335ae76fb1696459430d42f733b8cb0d7d65b84d3/yarl-1.25.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:29273edf1530e397bd07cb784db1fbe0d2590b77569f2e24679a9c0a2d763b94", size = 107361, upload-time = "2026-09-15T19:32:58.827Z" }, + { url = "https://files.pythonhosted.org/packages/ed/70/66e8c76b569b450d16e190f15071c916c3df70b0e33927e415ac497cf0c2/yarl-1.25.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b7abffdf37af1cec6a2ad69b827aa84320db5894791bc8ed932dc93fb274b7e9", size = 114396, upload-time = "2026-09-15T19:33:02.24Z" }, + { url = "https://files.pythonhosted.org/packages/73/23/0d82838a05c57fdc05bc8b66e8c92dcc0df15e27463a5f163142d521c682/yarl-1.25.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2239a02249d9326655419e0168a28ca9008938eaab31dc29fc875c217927a6c0", size = 104882, upload-time = "2026-09-15T19:33:04.494Z" }, + { url = "https://files.pythonhosted.org/packages/86/d4/ea08615c4edaa6049a13a2f1128944d068d1893abda7d708d4d7ea01599a/yarl-1.25.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:664ec6a520b74a1df2810666eb67695fcb77fa663e6ea0a25aaf2e529cb24dfa", size = 119485, upload-time = "2026-09-15T19:33:06.583Z" }, + { url = "https://files.pythonhosted.org/packages/1a/82/0898bdce9b1ae403b308b9c733d0d24af4a3464270c2c081f457b16c3e0d/yarl-1.25.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4f1c91f5a5980a937ff8e238e98e6897e1ad74a4b1e2c0d68c73b5ffbb3f5c0b", size = 122490, upload-time = "2026-09-15T19:33:08.653Z" }, + { url = "https://files.pythonhosted.org/packages/d1/38/97d79b81c342b78246cfedb74809e68841f3198d21653e10d3232bd9c622/yarl-1.25.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7c88edaec8c349ad4c5ad4c486a3defcc4b80ceb2f074436ffa0a87caf5e76a6", size = 115336, upload-time = "2026-09-15T19:33:11.056Z" }, + { url = "https://files.pythonhosted.org/packages/8e/9d/2577896554cd310dc470adb6da0b7dd0b435cb63e2565204a7ac240e504c/yarl-1.25.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:35dcbea443fafb3eece757ad4e514560ddeb6c34cfae1582c620d7b293d7feee", size = 111825, upload-time = "2026-09-15T19:33:13.204Z" }, + { url = "https://files.pythonhosted.org/packages/29/6b/7ac49d8ba84a5c4bd73415a4c949d22c749cb3762579b3d50e48019a78aa/yarl-1.25.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:882569ff613758cac762a457a5d72d6e211b28d4bcfea89d1d71ea942b02eac0", size = 114655, upload-time = "2026-09-15T19:33:15.553Z" }, + { url = "https://files.pythonhosted.org/packages/e5/18/e5942a16723f5b72f9b1297fd5a85a54f6300cd15c0dcb5005b90cd89156/yarl-1.25.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:d0f1489233a254bb3643d2f05de7d59019254d81daeca6b9162fe9edef57e0c7", size = 106395, upload-time = "2026-09-15T19:33:17.599Z" }, + { url = "https://files.pythonhosted.org/packages/7b/2d/549fa46240781513ebc47ae7eb418df428a163a2a3d644cc9cbb3ecb7846/yarl-1.25.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:f41753a76f4f63927d03a0d8ba8f5ce0f2083bec29a8cfaccc55371b1564b96b", size = 119277, upload-time = "2026-09-15T19:33:19.973Z" }, + { url = "https://files.pythonhosted.org/packages/76/16/4763f78dcdc0b3b9fb3842b04afe72b9320857c6a69300c62a0eab03d119/yarl-1.25.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:8fb0eb4955adf0579001581f2f71a126e8781ba61bcd120f127b0401163c6c2d", size = 112504, upload-time = "2026-09-15T19:33:22.464Z" }, + { url = "https://files.pythonhosted.org/packages/ae/b4/974e3edfe0d188393ce1cb9de400111c63fe61f4eb3b772a500d84c970d1/yarl-1.25.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:a1e32763e641a1566507d90a8d3b19bfc3cc04a9d4e5ae3e32189874ed4b58a3", size = 116243, upload-time = "2026-09-15T19:33:24.788Z" }, + { url = "https://files.pythonhosted.org/packages/b0/aa/157b940428da80c104ca09666a740e51c94963df65d5b112e06b52e4d7a8/yarl-1.25.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:65b5b2066651b7432d389e9799d979c703bcc6ef44266bb8153ef54e91e4aab3", size = 115822, upload-time = "2026-09-15T19:33:26.886Z" }, + { url = "https://files.pythonhosted.org/packages/7e/af/19fbdce41412e1b96825544cc52cd7029d3724655d0988237972f078bd29/yarl-1.25.1-cp314-cp314t-win_amd64.whl", hash = "sha256:734f6e5400352ac4254456003d462866c684703570929cff7a7bde015d0cb371", size = 107386, upload-time = "2026-09-15T19:33:29.009Z" }, + { url = "https://files.pythonhosted.org/packages/2a/99/f6431c8968e89be608d74b28ae2d024521b2953f27dd44e0dece5e04f67a/yarl-1.25.1-cp314-cp314t-win_arm64.whl", hash = "sha256:287e99ff5aa4dc1c7630bfc683ded6f106d756c99dec432a2d7f197a784f51c6", size = 102094, upload-time = "2026-09-15T19:33:31.151Z" }, + { url = "https://files.pythonhosted.org/packages/c7/3b/4f51eab40c2eabea6c3d5b121dff4b8988dc35087732ffede12d2be8b8dd/yarl-1.25.1-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:9b1bdaae98bc016825dd3c9d8ee1832f829b3341f9cc6ebd1a1b0a7fef7367cc", size = 143875, upload-time = "2026-09-15T19:33:33.52Z" }, + { url = "https://files.pythonhosted.org/packages/41/05/bbd58fc063f5f299a883f810760b265ca26c8167c91cb9a494d0fe2387e1/yarl-1.25.1-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:e7011b8fb8c4054bf0c12e5edc6cd83778b0028e99ce59b18586ed036f92cfdc", size = 104028, upload-time = "2026-09-15T19:33:36.22Z" }, + { url = "https://files.pythonhosted.org/packages/12/ee/2fba0aecb52e7020e189f684148783aa0b9cfa3b3bfb0b400646eef70ad4/yarl-1.25.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:f074e8d4aa0a5798920ddb6de3d08b228c614ff3724c3e8bd7577f4bafea867b", size = 104041, upload-time = "2026-09-15T19:33:38.86Z" }, + { url = "https://files.pythonhosted.org/packages/38/35/884beab53ed88c7247d1671972b5ef116f7351fe0c7e6de8c3558372cb16/yarl-1.25.1-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7d42e7e3ca399555578b4d617e3a6ecf13371b3743a115995fa010c7bf341459", size = 116018, upload-time = "2026-09-15T19:33:44.265Z" }, + { url = "https://files.pythonhosted.org/packages/e2/cc/1a91b685afb55cc18608443ace95280e96e263a97565811732b6788d3269/yarl-1.25.1-cp315-cp315-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:aa4ed3dd308548f9e707d9caaf005d2d7f8c1e7868f858dfeb47fe76e16b391d", size = 107033, upload-time = "2026-09-15T19:33:46.45Z" }, + { url = "https://files.pythonhosted.org/packages/c5/c1/58b379fcb1d68d907b7fcf75200c44321896509b2a6a74abbb4b19d864d2/yarl-1.25.1-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:42a66563d8cc056ee32e6191e05097a7b2b3bc302e0bc3133daf8710eb18bd26", size = 123257, upload-time = "2026-09-15T19:33:48.631Z" }, + { url = "https://files.pythonhosted.org/packages/d7/2d/1fe96cf5c2aeab10095e48f38585cf5a8451fb7253234822398e52aa5336/yarl-1.25.1-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:98d370568f393215d605304cdb77b3d5539bd192c75b623c7304c42c8d6d8273", size = 126745, upload-time = "2026-09-15T19:33:50.999Z" }, + { url = "https://files.pythonhosted.org/packages/ad/60/8674394ce43f4dadae573a1d6f451716438e9eab7d7fe8d643c673a32d85/yarl-1.25.1-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:23bf5b403c879a54964e0feac7285688e04bb220074878d737d331522da0a5bf", size = 117255, upload-time = "2026-09-15T19:33:53.456Z" }, + { url = "https://files.pythonhosted.org/packages/2f/72/0faa30e02605d56127d42bb987dcc97da3863b7bf70b9bfbf5f739c05e30/yarl-1.25.1-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d45673badd08456d0340e9364eddafe1c53a9d2896424294de4d7dd71ad3ee57", size = 115166, upload-time = "2026-09-15T19:33:55.669Z" }, + { url = "https://files.pythonhosted.org/packages/8c/90/9a46eac564c437e128285c5c1d7bb385d268394e9209d84f6bf4a14471ef/yarl-1.25.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:0136d640dfa9b0523853e411430a99f8a91eca85774c6420285a33b755bc6de3", size = 116082, upload-time = "2026-09-15T19:33:57.78Z" }, + { url = "https://files.pythonhosted.org/packages/ad/38/4b1a686a3758878f93d2f1ea943f5a165f3555769cd16e761cfd0efdca17/yarl-1.25.1-cp315-cp315-musllinux_1_2_armv7l.whl", hash = "sha256:59ba3a6e1aa8cfe5adf4bd270fd965db21955401b7ca6f1696010c55ed4daec2", size = 108048, upload-time = "2026-09-15T19:34:00.25Z" }, + { url = "https://files.pythonhosted.org/packages/da/14/f348eb967f31a58348612a2b93bd8a2ab664548e2b5e879cac7f592f7201/yarl-1.25.1-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:87796fedc3ba97ec14fab55acb48584276e6c1e4c1e89c422bda62c838e754a9", size = 122775, upload-time = "2026-09-15T19:34:02.455Z" }, + { url = "https://files.pythonhosted.org/packages/ab/e9/4f7b79700f88cb9e8bb66f8b54f9bce1844c013a2c39fdc47112e9334c95/yarl-1.25.1-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:bd0912757081f89b107d6c00b2ff8a194401b0b87eadcf4481de2b865a8fd44f", size = 115096, upload-time = "2026-09-15T19:34:05.283Z" }, + { url = "https://files.pythonhosted.org/packages/cf/37/f9cb020331997d3eb887bd28d5410ecfd3d80bf163c23d7cec490d78dade/yarl-1.25.1-cp315-cp315-musllinux_1_2_s390x.whl", hash = "sha256:b51c159a9794633f5e0db7ecec7b2b6e3734eca1f5d17dc989ff3552a43ff78b", size = 120655, upload-time = "2026-09-15T19:34:07.382Z" }, + { url = "https://files.pythonhosted.org/packages/12/83/52fceb22891a41f168db7ec22fd1d81e06b6a0b8d9f70921bd3e785defd0/yarl-1.25.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:319e070a01db9920fb63761843f96a104c8e2b9427266731810dc1e22595b17c", size = 117488, upload-time = "2026-09-15T19:34:09.988Z" }, + { url = "https://files.pythonhosted.org/packages/f5/5c/ce6c4ff1247fcbe4b33d462c23a097106d909b173fde7042bc52290466e2/yarl-1.25.1-cp315-cp315-win_amd64.whl", hash = "sha256:a2059a2d891bd156bc5184e7ab7a56e78a84dfcfdeac8c501b552533ad1c36ee", size = 103434, upload-time = "2026-09-15T19:34:12.56Z" }, + { url = "https://files.pythonhosted.org/packages/3c/a1/766a906b0704fb26d52b19dc22bed48a8ba0544203b70dcf44da350e8194/yarl-1.25.1-cp315-cp315-win_arm64.whl", hash = "sha256:a78b50b4f7918a3de71105d5c0b93bbc57bb8339a4d03a9dfd449f9068e76f3d", size = 99155, upload-time = "2026-09-15T19:34:15.132Z" }, + { url = "https://files.pythonhosted.org/packages/bd/d3/a1d09b32cb6ab14f66b44939f5b4255b8b9e747aef3974af1d5d80ccc2fd/yarl-1.25.1-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:b5402a340723fa7da00b5cff987ddab61276be6d11251ea71ae02bcac54890d8", size = 149284, upload-time = "2026-09-15T19:34:17.452Z" }, + { url = "https://files.pythonhosted.org/packages/7f/3b/fe554d879692650bca70bfbc0df124e82e4d2bb7456f698c7756f1279a96/yarl-1.25.1-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:eda19ea5ee88742f47a2340816e6f2d40b53bed3ab5b69794769f36af9f35bb4", size = 106399, upload-time = "2026-09-15T19:34:21.474Z" }, + { url = "https://files.pythonhosted.org/packages/e1/4b/e7af56177ac8d40094c82d7728224c0b8472157d50d362e5fb3b014b2bc8/yarl-1.25.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:75baa6cf9b6d1c52f3e111a130e202fd8cf0a5b3a066c3f73d615e885092e4ec", size = 106968, upload-time = "2026-09-15T19:34:23.619Z" }, + { url = "https://files.pythonhosted.org/packages/da/4f/2df41fd738d46f23ef829ae8b4468d94bb6070038fc6dfab6165ed44fea8/yarl-1.25.1-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dbcef5a9119ef653653132cccaf999b30a0af6f33bb0a4ba80bec30056868487", size = 114717, upload-time = "2026-09-15T19:34:25.879Z" }, + { url = "https://files.pythonhosted.org/packages/69/ea/002b66df53bbd1aed1c23358ff99c9bdc744fe3f4d2740e1b2fdd7192885/yarl-1.25.1-cp315-cp315t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7efc9f082dfed77c316edffa9deb52888e1bc6789171887cc1f68e06d65465c8", size = 105198, upload-time = "2026-09-15T19:34:28.204Z" }, + { url = "https://files.pythonhosted.org/packages/b1/7c/95c8bc0c8f97d71e59c94525ad60d76f5c57d3f2820f08137ca8b9f0542a/yarl-1.25.1-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:fe01645169a2112aa1d4ebc3e4c5f029c5c8f97adfc32e5d37c993b39a994d75", size = 120271, upload-time = "2026-09-15T19:34:30.5Z" }, + { url = "https://files.pythonhosted.org/packages/5d/7a/6fe9da56ec77927baa669fd86c39c567ce6205bab53d581082c6744c8ae7/yarl-1.25.1-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1c557dfd5e3db046053a0bdc72261ade790ebe8e2c7a41b36b0ca1f14cb95f3", size = 123572, upload-time = "2026-09-15T19:34:32.73Z" }, + { url = "https://files.pythonhosted.org/packages/8b/83/35f222d17fa70a14c7c74fdf112ccf5515e0c2a87082b1f9b99f7693bf57/yarl-1.25.1-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8ce4d6ccafb33d39bd78444612d14938ead674c25702ded2ee9c54a47735d225", size = 115228, upload-time = "2026-09-15T19:34:35.344Z" }, + { url = "https://files.pythonhosted.org/packages/da/6f/fbaaf619423578a7d898d0f226ae47bc1906c293865c416d83c17b828b0e/yarl-1.25.1-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:80a063f8297fc796296f00f100be520f209b23dc98f93ce8eba6ee7122598209", size = 111618, upload-time = "2026-09-15T19:34:37.656Z" }, + { url = "https://files.pythonhosted.org/packages/ba/78/7383278f1b3cf8e0496bd95b3281a7b09b89217b6b428db24c6b99b3deca/yarl-1.25.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:7cb414a73e21a7ab58254926073f2930cb22f5b4314ea4260a687e2b3fd4dce3", size = 114839, upload-time = "2026-09-15T19:34:40.099Z" }, + { url = "https://files.pythonhosted.org/packages/62/49/5506e5b6d29aab91bd845cc9016d88c3d3f81b81bc242b8100bdd5737825/yarl-1.25.1-cp315-cp315t-musllinux_1_2_armv7l.whl", hash = "sha256:85a18376073f8a39aa07be34f9fc77e2869aa72c55c441efdd2cf79a0407504d", size = 106212, upload-time = "2026-09-15T19:34:42.768Z" }, + { url = "https://files.pythonhosted.org/packages/46/8a/19877c193b7c5929f4b07118c18bbe390f3fadd0f59dd98c0cd12b31fa5c/yarl-1.25.1-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:77716e245c90f058466a05e6a465bb8600f767a8f4b18b4d40f3aff958e5f73c", size = 119985, upload-time = "2026-09-15T19:34:44.976Z" }, + { url = "https://files.pythonhosted.org/packages/4e/6c/0a46fbbf9ecbcbd0cc20d2193394254b9e19817f22c814aab60f99847400/yarl-1.25.1-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:1e80dcf1446e1b080b1932b0d103c464a04112f5bc31f0f983ad418172063cde", size = 112081, upload-time = "2026-09-15T19:34:47.45Z" }, + { url = "https://files.pythonhosted.org/packages/ef/30/93f5d471230c74ccd06255d0842739f86551f937f9e63a5e947853c6244a/yarl-1.25.1-cp315-cp315t-musllinux_1_2_s390x.whl", hash = "sha256:bdc8d8b8c22e9e43ac68316b5e6cf083dec537f4ec213cb4aa967b583bc3fa64", size = 116995, upload-time = "2026-09-15T19:34:49.972Z" }, + { url = "https://files.pythonhosted.org/packages/2b/80/c386593035ee3f9c6c6af0847b5578f2830c674794a9d7701b744a3ebd42/yarl-1.25.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:dfbf531053a0935f2e871bcd4753f90313688772ff8c017f5ea402e315a78c1f", size = 115570, upload-time = "2026-09-15T19:34:52.628Z" }, + { url = "https://files.pythonhosted.org/packages/38/02/eef443559563ef8f2e10469387b8b1e97cb5efee95b288a56da60801f7ee/yarl-1.25.1-cp315-cp315t-win_amd64.whl", hash = "sha256:b13b88747769537f3d32e89e3a735da10c0a9e35d7322928c701b5f93d3afffd", size = 106811, upload-time = "2026-09-15T19:34:54.935Z" }, + { url = "https://files.pythonhosted.org/packages/88/91/41e284ca2cf5211e05dae031d126a3668aea88fa759df56e7e35c6ad25ba/yarl-1.25.1-cp315-cp315t-win_arm64.whl", hash = "sha256:783dd1467083f4d3f7722ad6a313f24c173e7571372738fcb7a6e6d1ba48df25", size = 101804, upload-time = "2026-09-15T19:34:57.231Z" }, + { url = "https://files.pythonhosted.org/packages/54/22/318c7980066769c6bcd9221ed2248294f5698811da099013098c670565ed/yarl-1.25.1-py3-none-any.whl", hash = "sha256:681c758b0490f9e96b78e5fa8e8dc6e648e9185bb6eaebe73183c33ea0c445f3", size = 63617, upload-time = "2026-09-15T19:34:59.616Z" }, +] + +[[package]] +name = "zipp" +version = "4.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b9/d8/eab98a517c14134c0b2eb4e2387bc5f457334293ec5d2dd3857ec2966802/zipp-4.1.0.tar.gz", hash = "sha256:4cb57381f544315db7688e976e922a2b18cdb513d21cc194eb42232ba2a3e602", size = 26214, upload-time = "2026-05-18T20:08:57.967Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3a/13/547360d81e6d88d58492968ffda9f9542854f11310ee556fef14260cc886/zipp-4.1.0-py3-none-any.whl", hash = "sha256:25ad4e16390cd314347dd8f1de67a2ac538ae658ed4ab9db16029c07c188e97f", size = 10238, upload-time = "2026-05-18T20:08:57.045Z" }, +] From c5e0e6e29c69b487dfc89764d4d79eddfaa427ae Mon Sep 17 00:00:00 2001 From: vmihalovski Date: Thu, 17 Sep 2026 17:45:03 +0200 Subject: [PATCH 2/4] Addressed Copilot comments --- converters/ontology/src/ossie_ontology/expr/formula/lexer.py | 2 +- converters/ontology/src/ossie_ontology/expr/model.py | 2 +- .../tests/palantir/converter/test_object_type_statuses.py | 2 +- .../ontology/tests/palantir/converter/test_relation_statuses.py | 1 - 4 files changed, 3 insertions(+), 4 deletions(-) diff --git a/converters/ontology/src/ossie_ontology/expr/formula/lexer.py b/converters/ontology/src/ossie_ontology/expr/formula/lexer.py index 208577cc..7f8c9764 100644 --- a/converters/ontology/src/ossie_ontology/expr/formula/lexer.py +++ b/converters/ontology/src/ossie_ontology/expr/formula/lexer.py @@ -123,4 +123,4 @@ def t_ID(self, t): # every string that doesn't match one of the previous tokens is considered an error def t_error(self, t): r'.' - return SyntaxError(f'Illegal character {t.value!r} at position {t.lexpos}') + raise SyntaxError(f"Illegal character {t.value!r} at position {t.lexpos}") diff --git a/converters/ontology/src/ossie_ontology/expr/model.py b/converters/ontology/src/ossie_ontology/expr/model.py index d531abbb..b1de2016 100644 --- a/converters/ontology/src/ossie_ontology/expr/model.py +++ b/converters/ontology/src/ossie_ontology/expr/model.py @@ -30,7 +30,7 @@ class BinOp(str, Node, Enum): LE = "<=" GT = ">" GE = ">=" - comparison_ops = {EQ, NE, LE, LE, GT, GE} + comparison_ops = {EQ, NE, LT, LE, GT, GE} # arithmetic operators PLUS = "+" diff --git a/converters/ontology/tests/palantir/converter/test_object_type_statuses.py b/converters/ontology/tests/palantir/converter/test_object_type_statuses.py index 6ada313d..d816e007 100644 --- a/converters/ontology/tests/palantir/converter/test_object_type_statuses.py +++ b/converters/ontology/tests/palantir/converter/test_object_type_statuses.py @@ -31,7 +31,7 @@ from tests.palantir.converter.helpers import _convert, _concept_names -def test_default_policy_admits_active_endorsed_andintermediary(tmp_path: Path): +def test_default_policy_admits_active_endorsed_and_intermediary(tmp_path: Path): model = _convert(tmp_path, [ object_type("alpha", "Alpha", status="active"), object_type("bravo", "Bravo", status="endorsed"), diff --git a/converters/ontology/tests/palantir/converter/test_relation_statuses.py b/converters/ontology/tests/palantir/converter/test_relation_statuses.py index 4bee3ec5..67cb8824 100644 --- a/converters/ontology/tests/palantir/converter/test_relation_statuses.py +++ b/converters/ontology/tests/palantir/converter/test_relation_statuses.py @@ -39,7 +39,6 @@ _concept_names, _relationship_names, _mapped_relationship_names, - _linked_export, ) From c8f6d43bfd0def2110c902e7c8b8f70c2cd69ade Mon Sep 17 00:00:00 2001 From: vmihalovski Date: Thu, 17 Sep 2026 17:55:28 +0200 Subject: [PATCH 3/4] Addressed Copilot comments --- converters/ontology/src/ossie_ontology/expr/model.py | 2 +- converters/ontology/tests/conftest.py | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/converters/ontology/src/ossie_ontology/expr/model.py b/converters/ontology/src/ossie_ontology/expr/model.py index b1de2016..ad59d185 100644 --- a/converters/ontology/src/ossie_ontology/expr/model.py +++ b/converters/ontology/src/ossie_ontology/expr/model.py @@ -66,7 +66,7 @@ def op(self): return self._op def __hash__(self): - return hash((self._op, *([a.__hash__() for a in self._args]))) + return hash((self._op, *self._args)) def __str__(self): left, right = self._args diff --git a/converters/ontology/tests/conftest.py b/converters/ontology/tests/conftest.py index d063a55a..264a80a3 100644 --- a/converters/ontology/tests/conftest.py +++ b/converters/ontology/tests/conftest.py @@ -160,9 +160,10 @@ def flights_model(flights_path: Path) -> OssieOntology: def spec_model(request) -> OssieOntology: """The parsed ontology for the spec named by an indirect parametrize. - Formulas stay as raw text: these suites snapshot the shape of the spec, and - parsing them would pull the optional `relationalai` extra in through the - formula factories for no gain. + Built with the default factories, so formulas arrive parsed and validated. + That needs nothing optional: the grammar lives in `ossie_ontology.expr` and + depends only on `ply`, while the `relationalai` extra is what the PyRel + emitter needs -- a different layer, and not one these suites reach. """ return OssieParser().parse(_SPECS_DIR / f"{request.param}.yaml") From 7466adf99345d8502a8abfde978de414d1cd8414 Mon Sep 17 00:00:00 2001 From: vmihalovski Date: Thu, 17 Sep 2026 23:00:48 +0200 Subject: [PATCH 4/4] Addressed review comments --- converters/ontology/README.md | 54 ++++++++++++++++--- converters/ontology/pyproject.toml | 14 +++-- .../ossie_to_relationalai/converter.py | 6 +-- .../tests/relationalai/test_converter.py | 3 +- 4 files changed, 60 insertions(+), 17 deletions(-) diff --git a/converters/ontology/README.md b/converters/ontology/README.md index 01a68e3f..054dd20e 100644 --- a/converters/ontology/README.md +++ b/converters/ontology/README.md @@ -30,16 +30,36 @@ Converters between Ossie, Palantir, Spec, and RelationalAI ontology formats. ### The `relationalai` extra -`ossie_to_relationalai` is the only converter that needs a vendor SDK, so it is -gated behind an optional extra rather than the base install: +> **`relationalai` is proprietary and declares no license.** Its PyPI +> distribution carries no `License` field, no license classifier and no +> `LICENSE` file, and the [project page](https://pypi.org/project/relationalai/) +> lists none either — so it grants no use or redistribution rights by default. +> Get the applicable terms from RelationalAI (`support@relational.ai`) before +> installing it. Ossie is Apache-2.0 and neither bundles nor depends on it. +> +> Installing the package is also not enough to *run* what this converter emits. +> That needs a Snowflake account with the RelationalAI Native App installed from +> the [Marketplace](https://app.snowflake.com/marketplace/listing/GZTYZOOIX8H/relationalai-relationalai), +> and access enabled by RelationalAI on request — see the +> [setup guide](https://docs.relational.ai/manage/get-started/install/). Ossie +> never executes the generated source: it converts and compiles in-process, +> offline, against no engine. + +`ossie_to_relationalai` is the only converter that needs a vendor SDK. Because +that SDK is non-free, it is kept out of the base install entirely and gated +behind an opt-in extra, so nobody acquires it without asking for it: ```bash -pip install "apache-ossie-ontology[relationalai]" +pip install "apache-ossie-ontology[relationalai]" # opt in, having read the above +pip install apache-ossie-ontology # everything else, no vendor SDK ``` Nothing reachable from `ossie_ontology/__init__.py` imports `relationalai`, so everything else — parsing Ossie, converting Palantir, reading and writing the -spec — installs and runs without it. Its tests skip themselves when it is absent. +spec — installs and runs without it. `tests/test_optional_extra.py` asserts that +boundary in a subprocess with the SDK masked, so the base install cannot start +depending on it by accident. The converter's own tests skip themselves when it +is absent. The PyRel-side model it targets — `OntologyModel` and its bindings, roles and CSV plumbing — lives under `ossie_ontology/vendor/relationalai/`, @@ -77,10 +97,30 @@ Two things about the environment this needs. Constructing a model makes And each dataset's `source` is read from the configured connection to get its column types, so the call above needs one that can reach those tables. -To convert without a warehouse, pass `use_csv_only=True`: every dataset is then -treated as an inline CSV rather than looked up, and nothing leaves the process. +To convert without a warehouse, pass a different `table_provider`. The default, +`warehouse_table`, resolves each dataset's `source` against the connection — +which is what gets the column identifiers right, since Snowflake folds an +unquoted name to upper case. The alternative, `declared_table`, declares the +columns from the Ossie spec instead and reads nothing: + +```python +from ossie_ontology.converter.ossie_to_relationalai import declared_table + +model = OssieToRelationalAIConverter.convert(ontology, table_provider=declared_table) +``` + +Nothing leaves the process, so the tables the generated source names need not +exist. The tradeoff is the one `warehouse_table` avoids: the declared +identifiers have to match how the table was actually created. + That is how the test suite runs — see `tests/conftest.py` for the offline config -it pins, and `tests/test_ossie_to_relationalai.py` for the full offline setup. +it pins and the network guard it installs, and `tests/relationalai/test_converter.py` +for the rest of the offline setup. + +`table_provider` is also the extension point for reading rows from somewhere +else entirely — from inline CSV, for example, so a test corpus needs no +warehouse. This package ships no such provider and has no CSV handling at all, +because reading rows is not part of converting an ontology. ## Prerequisites diff --git a/converters/ontology/pyproject.toml b/converters/ontology/pyproject.toml index 6ff2e786..dfdf8da1 100644 --- a/converters/ontology/pyproject.toml +++ b/converters/ontology/pyproject.toml @@ -44,11 +44,15 @@ dependencies = [ "pyyaml", ] -# Extras are opt-in: `pip install "apache-ossie-ontology[relationalai]"`. -# `relationalai` is the vendor SDK the Ossie -> RelationalAI converter emits -# against. Parsing Ossie, converting Palantir, and reading/writing the spec all -# work without it, and nothing reachable from `ossie_ontology/__init__.py` -# imports it — so it stays out of the base install. +# Opt-in extra: `pip install "apache-ossie-ontology[relationalai]"`. +# +# WARNING: `relationalai` is proprietary and declares no license, and running +# what this converter emits needs a RelationalAI deployment in Snowflake. Read +# README.md, "The `relationalai` extra", before installing it. +# +# It is an extra rather than a dependency because nothing reachable from +# `ossie_ontology/__init__.py` imports it: everything but this one converter +# works without the SDK. [project.optional-dependencies] relationalai = [ "relationalai==1.27.1", diff --git a/converters/ontology/src/ossie_ontology/converter/ossie_to_relationalai/converter.py b/converters/ontology/src/ossie_ontology/converter/ossie_to_relationalai/converter.py index 4bcf7ee9..7474d26b 100644 --- a/converters/ontology/src/ossie_ontology/converter/ossie_to_relationalai/converter.py +++ b/converters/ontology/src/ossie_ontology/converter/ossie_to_relationalai/converter.py @@ -57,9 +57,9 @@ # from that spec; returns whatever pyrel construct should stand in for it. # # The default resolves the dataset's `source` to a warehouse table. Substituting -# a provider is how a caller reads from somewhere else — a downstream project -# supplies one backed by inline CSV so its tests need no warehouse — without -# this converter having to know that any such alternative exists. +# a provider is how a caller reads from somewhere else — from inline CSV, for +# example, so a test corpus needs no warehouse — without this converter having +# to know that any such alternative exists. TableProvider = Callable[["OntologyModel", Dataset, dict[str, RAIConcept]], Any] diff --git a/converters/ontology/tests/relationalai/test_converter.py b/converters/ontology/tests/relationalai/test_converter.py index e8c13e52..16e98f41 100644 --- a/converters/ontology/tests/relationalai/test_converter.py +++ b/converters/ontology/tests/relationalai/test_converter.py @@ -79,8 +79,7 @@ def convert(spec: str, specs_dir: Path) -> OntologyModel: how its identifiers were cased — which is exactly why these tests pick. No rows are supplied and none are needed; reading real data is an extension - (a downstream project supplies a CSV-backed provider), not something this - converter knows. + — a CSV-backed provider, for example — not something this converter knows. """ # pyrel keeps constructed models in a process-wide registry; clearing it # keeps each case independent and its generated names stable.