lorebase-core 0.2.3__tar.gz → 0.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {lorebase_core-0.2.3 → lorebase_core-0.3.0}/PKG-INFO +1 -1
- {lorebase_core-0.2.3 → lorebase_core-0.3.0}/pyproject.toml +1 -1
- lorebase_core-0.3.0/src/lorebase_core/__init__.py +1 -0
- {lorebase_core-0.2.3 → lorebase_core-0.3.0}/src/lorebase_core/canonical/hashing.py +1 -0
- {lorebase_core-0.2.3 → lorebase_core-0.3.0}/src/lorebase_core/expr/visibility.py +23 -0
- lorebase_core-0.3.0/src/lorebase_core/infer/phrases.py +40 -0
- {lorebase_core-0.2.3 → lorebase_core-0.3.0}/src/lorebase_core/infer/suggestions.py +277 -15
- {lorebase_core-0.2.3 → lorebase_core-0.3.0}/src/lorebase_core/merge/__init__.py +2 -0
- {lorebase_core-0.2.3 → lorebase_core-0.3.0}/src/lorebase_core/merge/data_merge.py +90 -4
- {lorebase_core-0.2.3 → lorebase_core-0.3.0}/src/lorebase_core/merge/engine.py +71 -2
- {lorebase_core-0.2.3 → lorebase_core-0.3.0}/src/lorebase_core/merge/objects.py +2 -0
- {lorebase_core-0.2.3 → lorebase_core-0.3.0}/src/lorebase_core/merge/policies.py +27 -1
- {lorebase_core-0.2.3 → lorebase_core-0.3.0}/src/lorebase_core/merge/schema_merge.py +1 -0
- lorebase_core-0.3.0/src/lorebase_core/merge/text_merge.py +297 -0
- {lorebase_core-0.2.3 → lorebase_core-0.3.0}/src/lorebase_core/model/__init__.py +10 -0
- lorebase_core-0.3.0/src/lorebase_core/model/attribute.py +161 -0
- {lorebase_core-0.2.3 → lorebase_core-0.3.0}/src/lorebase_core/model/base.py +16 -0
- {lorebase_core-0.2.3 → lorebase_core-0.3.0}/src/lorebase_core/model/domain.py +18 -1
- {lorebase_core-0.2.3 → lorebase_core-0.3.0}/src/lorebase_core/model/ids.py +2 -0
- {lorebase_core-0.2.3 → lorebase_core-0.3.0}/src/lorebase_core/model/kind.py +32 -4
- lorebase_core-0.3.0/src/lorebase_core/model/lens.py +32 -0
- {lorebase_core-0.2.3 → lorebase_core-0.3.0}/src/lorebase_core/model/lineage.py +58 -2
- {lorebase_core-0.2.3 → lorebase_core-0.3.0}/src/lorebase_core/model/schema.py +13 -0
- {lorebase_core-0.2.3 → lorebase_core-0.3.0}/src/lorebase_core/model/source.py +58 -1
- {lorebase_core-0.2.3 → lorebase_core-0.3.0}/src/lorebase_core/model/yaml_schema.py +29 -0
- lorebase_core-0.3.0/src/lorebase_core/reconcile/__init__.py +122 -0
- lorebase_core-0.3.0/src/lorebase_core/reconcile/candidates.py +236 -0
- lorebase_core-0.3.0/src/lorebase_core/reconcile/claims.py +200 -0
- lorebase_core-0.3.0/src/lorebase_core/reconcile/detect.py +342 -0
- lorebase_core-0.3.0/src/lorebase_core/reconcile/normalize.py +301 -0
- lorebase_core-0.3.0/src/lorebase_core/reconcile/options.py +229 -0
- lorebase_core-0.3.0/src/lorebase_core/reconcile/units.yaml +43 -0
- {lorebase_core-0.2.3 → lorebase_core-0.3.0}/src/lorebase_core/validate/issues.py +4 -1
- {lorebase_core-0.2.3 → lorebase_core-0.3.0}/src/lorebase_core/validate/schema_rules.py +149 -1
- {lorebase_core-0.2.3 → lorebase_core-0.3.0}/tests/canonical/vectors/object_hashes.json +9 -0
- lorebase_core-0.3.0/tests/expr/test_lens.py +145 -0
- {lorebase_core-0.2.3 → lorebase_core-0.3.0}/tests/infer/test_suggestions.py +159 -0
- lorebase_core-0.3.0/tests/merge/test_preserved_base.py +94 -0
- {lorebase_core-0.2.3 → lorebase_core-0.3.0}/tests/merge/test_resolutions.py +44 -0
- lorebase_core-0.3.0/tests/merge/test_text_merge.py +556 -0
- lorebase_core-0.3.0/tests/model/test_resolution_fields.py +132 -0
- {lorebase_core-0.2.3 → lorebase_core-0.3.0}/tests/model/test_yaml_schema.py +29 -0
- lorebase_core-0.3.0/tests/reconcile/test_detect.py +311 -0
- lorebase_core-0.3.0/tests/reconcile/test_model.py +110 -0
- lorebase_core-0.3.0/tests/reconcile/test_reconcile.py +440 -0
- lorebase_core-0.3.0/tests/validate/__init__.py +0 -0
- {lorebase_core-0.2.3 → lorebase_core-0.3.0}/tests/validate/test_schema_rules.py +106 -0
- lorebase_core-0.2.3/src/lorebase_core/__init__.py +0 -1
- lorebase_core-0.2.3/src/lorebase_core/model/attribute.py +0 -72
- {lorebase_core-0.2.3 → lorebase_core-0.3.0}/.gitignore +0 -0
- {lorebase_core-0.2.3 → lorebase_core-0.3.0}/src/lorebase_core/canonical/__init__.py +0 -0
- {lorebase_core-0.2.3 → lorebase_core-0.3.0}/src/lorebase_core/canonical/jcs.py +0 -0
- {lorebase_core-0.2.3 → lorebase_core-0.3.0}/src/lorebase_core/expr/__init__.py +0 -0
- {lorebase_core-0.2.3 → lorebase_core-0.3.0}/src/lorebase_core/expr/ast.py +0 -0
- {lorebase_core-0.2.3 → lorebase_core-0.3.0}/src/lorebase_core/expr/checker.py +0 -0
- {lorebase_core-0.2.3 → lorebase_core-0.3.0}/src/lorebase_core/expr/compiler.py +0 -0
- {lorebase_core-0.2.3 → lorebase_core-0.3.0}/src/lorebase_core/expr/context.py +0 -0
- {lorebase_core-0.2.3 → lorebase_core-0.3.0}/src/lorebase_core/expr/derivations.py +0 -0
- {lorebase_core-0.2.3 → lorebase_core-0.3.0}/src/lorebase_core/expr/environment.py +0 -0
- {lorebase_core-0.2.3 → lorebase_core-0.3.0}/src/lorebase_core/expr/errors.py +0 -0
- {lorebase_core-0.2.3 → lorebase_core-0.3.0}/src/lorebase_core/expr/evaluator.py +0 -0
- {lorebase_core-0.2.3 → lorebase_core-0.3.0}/src/lorebase_core/expr/functions.py +0 -0
- {lorebase_core-0.2.3 → lorebase_core-0.3.0}/src/lorebase_core/expr/limits.py +0 -0
- {lorebase_core-0.2.3 → lorebase_core-0.3.0}/src/lorebase_core/expr/operations.py +0 -0
- {lorebase_core-0.2.3 → lorebase_core-0.3.0}/src/lorebase_core/expr/ops.py +0 -0
- {lorebase_core-0.2.3 → lorebase_core-0.3.0}/src/lorebase_core/expr/resolver.py +0 -0
- {lorebase_core-0.2.3 → lorebase_core-0.3.0}/src/lorebase_core/expr/rows.py +0 -0
- {lorebase_core-0.2.3 → lorebase_core-0.3.0}/src/lorebase_core/expr/sensitivity.py +0 -0
- {lorebase_core-0.2.3 → lorebase_core-0.3.0}/src/lorebase_core/expr/types.py +0 -0
- {lorebase_core-0.2.3 → lorebase_core-0.3.0}/src/lorebase_core/expr/validation.py +0 -0
- {lorebase_core-0.2.3 → lorebase_core-0.3.0}/src/lorebase_core/expr/values.py +0 -0
- {lorebase_core-0.2.3 → lorebase_core-0.3.0}/src/lorebase_core/infer/__init__.py +0 -0
- {lorebase_core-0.2.3 → lorebase_core-0.3.0}/src/lorebase_core/infer/columns.py +0 -0
- {lorebase_core-0.2.3 → lorebase_core-0.3.0}/src/lorebase_core/infer/engine.py +0 -0
- {lorebase_core-0.2.3 → lorebase_core-0.3.0}/src/lorebase_core/infer/keys.py +0 -0
- {lorebase_core-0.2.3 → lorebase_core-0.3.0}/src/lorebase_core/infer/names.py +0 -0
- {lorebase_core-0.2.3 → lorebase_core-0.3.0}/src/lorebase_core/infer/options.py +0 -0
- {lorebase_core-0.2.3 → lorebase_core-0.3.0}/src/lorebase_core/infer/profile.py +0 -0
- {lorebase_core-0.2.3 → lorebase_core-0.3.0}/src/lorebase_core/infer/proposal.py +0 -0
- {lorebase_core-0.2.3 → lorebase_core-0.3.0}/src/lorebase_core/infer/references.py +0 -0
- {lorebase_core-0.2.3 → lorebase_core-0.3.0}/src/lorebase_core/infer/tables.py +0 -0
- {lorebase_core-0.2.3 → lorebase_core-0.3.0}/src/lorebase_core/merge/conflicts.py +0 -0
- {lorebase_core-0.2.3 → lorebase_core-0.3.0}/src/lorebase_core/merge/consistency.py +0 -0
- {lorebase_core-0.2.3 → lorebase_core-0.3.0}/src/lorebase_core/merge/index.py +0 -0
- {lorebase_core-0.2.3 → lorebase_core-0.3.0}/src/lorebase_core/merge/resolve.py +0 -0
- {lorebase_core-0.2.3 → lorebase_core-0.3.0}/src/lorebase_core/merge/state.py +0 -0
- {lorebase_core-0.2.3 → lorebase_core-0.3.0}/src/lorebase_core/model/derivation.py +0 -0
- {lorebase_core-0.2.3 → lorebase_core-0.3.0}/src/lorebase_core/model/embedding.py +0 -0
- {lorebase_core-0.2.3 → lorebase_core-0.3.0}/src/lorebase_core/model/operation.py +0 -0
- {lorebase_core-0.2.3 → lorebase_core-0.3.0}/src/lorebase_core/model/paths.py +0 -0
- {lorebase_core-0.2.3 → lorebase_core-0.3.0}/src/lorebase_core/model/record.py +0 -0
- {lorebase_core-0.2.3 → lorebase_core-0.3.0}/src/lorebase_core/model/relation.py +0 -0
- {lorebase_core-0.2.3 → lorebase_core-0.3.0}/src/lorebase_core/model/trait.py +0 -0
- {lorebase_core-0.2.3 → lorebase_core-0.3.0}/src/lorebase_core/model/vault.py +0 -0
- {lorebase_core-0.2.3 → lorebase_core-0.3.0}/src/lorebase_core/query/__init__.py +0 -0
- {lorebase_core-0.2.3 → lorebase_core-0.3.0}/src/lorebase_core/query/dsl.py +0 -0
- {lorebase_core-0.2.3 → lorebase_core-0.3.0}/src/lorebase_core/query/resolve.py +0 -0
- {lorebase_core-0.2.3 → lorebase_core-0.3.0}/src/lorebase_core/revision/__init__.py +0 -0
- {lorebase_core-0.2.3 → lorebase_core-0.3.0}/src/lorebase_core/revision/ancestry.py +0 -0
- {lorebase_core-0.2.3 → lorebase_core-0.3.0}/src/lorebase_core/revision/changeset.py +0 -0
- {lorebase_core-0.2.3 → lorebase_core-0.3.0}/src/lorebase_core/revision/revision.py +0 -0
- {lorebase_core-0.2.3 → lorebase_core-0.3.0}/src/lorebase_core/tree/__init__.py +0 -0
- {lorebase_core-0.2.3 → lorebase_core-0.3.0}/src/lorebase_core/tree/diff.py +0 -0
- {lorebase_core-0.2.3 → lorebase_core-0.3.0}/src/lorebase_core/tree/hamt.py +0 -0
- {lorebase_core-0.2.3 → lorebase_core-0.3.0}/src/lorebase_core/tree/node.py +0 -0
- {lorebase_core-0.2.3 → lorebase_core-0.3.0}/src/lorebase_core/tree/params.py +0 -0
- {lorebase_core-0.2.3 → lorebase_core-0.3.0}/src/lorebase_core/tree/root.py +0 -0
- {lorebase_core-0.2.3 → lorebase_core-0.3.0}/src/lorebase_core/tree/store.py +0 -0
- {lorebase_core-0.2.3 → lorebase_core-0.3.0}/src/lorebase_core/types/__init__.py +0 -0
- {lorebase_core-0.2.3 → lorebase_core-0.3.0}/src/lorebase_core/types/casts.py +0 -0
- {lorebase_core-0.2.3 → lorebase_core-0.3.0}/src/lorebase_core/types/constraints.py +0 -0
- {lorebase_core-0.2.3 → lorebase_core-0.3.0}/src/lorebase_core/types/encoding.py +0 -0
- {lorebase_core-0.2.3 → lorebase_core-0.3.0}/src/lorebase_core/types/errors.py +0 -0
- {lorebase_core-0.2.3 → lorebase_core-0.3.0}/src/lorebase_core/types/matching.py +0 -0
- {lorebase_core-0.2.3 → lorebase_core-0.3.0}/src/lorebase_core/types/types.py +0 -0
- {lorebase_core-0.2.3 → lorebase_core-0.3.0}/src/lorebase_core/validate/__init__.py +0 -0
- {lorebase_core-0.2.3 → lorebase_core-0.3.0}/src/lorebase_core/validate/changes.py +0 -0
- {lorebase_core-0.2.3 → lorebase_core-0.3.0}/src/lorebase_core/validate/changeset.py +0 -0
- {lorebase_core-0.2.3 → lorebase_core-0.3.0}/src/lorebase_core/validate/data_rules.py +0 -0
- {lorebase_core-0.2.3 → lorebase_core-0.3.0}/tests/__init__.py +0 -0
- {lorebase_core-0.2.3 → lorebase_core-0.3.0}/tests/canonical/__init__.py +0 -0
- {lorebase_core-0.2.3 → lorebase_core-0.3.0}/tests/canonical/test_hashing.py +0 -0
- {lorebase_core-0.2.3 → lorebase_core-0.3.0}/tests/canonical/test_jcs.py +0 -0
- {lorebase_core-0.2.3 → lorebase_core-0.3.0}/tests/canonical/vectors/rfc8785/nested.expected.json +0 -0
- {lorebase_core-0.2.3 → lorebase_core-0.3.0}/tests/canonical/vectors/rfc8785/nested.input.json +0 -0
- {lorebase_core-0.2.3 → lorebase_core-0.3.0}/tests/canonical/vectors/rfc8785/numbers.json +0 -0
- {lorebase_core-0.2.3 → lorebase_core-0.3.0}/tests/canonical/vectors/rfc8785/sorting.expected.json +0 -0
- {lorebase_core-0.2.3 → lorebase_core-0.3.0}/tests/canonical/vectors/rfc8785/sorting.input.json +0 -0
- {lorebase_core-0.2.3 → lorebase_core-0.3.0}/tests/canonical/vectors/rfc8785/values.expected.json +0 -0
- {lorebase_core-0.2.3 → lorebase_core-0.3.0}/tests/canonical/vectors/rfc8785/values.input.json +0 -0
- {lorebase_core-0.2.3 → lorebase_core-0.3.0}/tests/expr/__init__.py +0 -0
- {lorebase_core-0.2.3 → lorebase_core-0.3.0}/tests/expr/support.py +0 -0
- {lorebase_core-0.2.3 → lorebase_core-0.3.0}/tests/expr/test_compile.py +0 -0
- {lorebase_core-0.2.3 → lorebase_core-0.3.0}/tests/expr/test_evaluate.py +0 -0
- {lorebase_core-0.2.3 → lorebase_core-0.3.0}/tests/expr/test_operations.py +0 -0
- {lorebase_core-0.2.3 → lorebase_core-0.3.0}/tests/expr/test_rows.py +0 -0
- {lorebase_core-0.2.3 → lorebase_core-0.3.0}/tests/expr/test_tag_origins.py +0 -0
- {lorebase_core-0.2.3 → lorebase_core-0.3.0}/tests/expr/test_validation.py +0 -0
- {lorebase_core-0.2.3 → lorebase_core-0.3.0}/tests/expr/test_visibility.py +0 -0
- {lorebase_core-0.2.3 → lorebase_core-0.3.0}/tests/infer/__init__.py +0 -0
- {lorebase_core-0.2.3 → lorebase_core-0.3.0}/tests/infer/support.py +0 -0
- {lorebase_core-0.2.3 → lorebase_core-0.3.0}/tests/infer/test_columns.py +0 -0
- {lorebase_core-0.2.3 → lorebase_core-0.3.0}/tests/infer/test_engine.py +0 -0
- {lorebase_core-0.2.3 → lorebase_core-0.3.0}/tests/infer/test_keys.py +0 -0
- {lorebase_core-0.2.3 → lorebase_core-0.3.0}/tests/infer/test_names.py +0 -0
- {lorebase_core-0.2.3 → lorebase_core-0.3.0}/tests/infer/test_references.py +0 -0
- {lorebase_core-0.2.3 → lorebase_core-0.3.0}/tests/infer/test_supply_gold.py +0 -0
- {lorebase_core-0.2.3 → lorebase_core-0.3.0}/tests/merge/__init__.py +0 -0
- {lorebase_core-0.2.3 → lorebase_core-0.3.0}/tests/merge/support.py +0 -0
- {lorebase_core-0.2.3 → lorebase_core-0.3.0}/tests/merge/test_conflict_kinds.py +0 -0
- {lorebase_core-0.2.3 → lorebase_core-0.3.0}/tests/merge/test_data_merge.py +0 -0
- {lorebase_core-0.2.3 → lorebase_core-0.3.0}/tests/merge/test_end_to_end.py +0 -0
- {lorebase_core-0.2.3 → lorebase_core-0.3.0}/tests/merge/test_index.py +0 -0
- {lorebase_core-0.2.3 → lorebase_core-0.3.0}/tests/merge/test_policies.py +0 -0
- {lorebase_core-0.2.3 → lorebase_core-0.3.0}/tests/merge/test_properties.py +0 -0
- {lorebase_core-0.2.3 → lorebase_core-0.3.0}/tests/merge/test_schema_merge.py +0 -0
- {lorebase_core-0.2.3 → lorebase_core-0.3.0}/tests/merge/test_virtual_base.py +0 -0
- {lorebase_core-0.2.3 → lorebase_core-0.3.0}/tests/model/__init__.py +0 -0
- {lorebase_core-0.2.3 → lorebase_core-0.3.0}/tests/model/supply.py +0 -0
- {lorebase_core-0.2.3 → lorebase_core-0.3.0}/tests/model/test_embedding.py +0 -0
- {lorebase_core-0.2.3 → lorebase_core-0.3.0}/tests/model/test_model.py +0 -0
- {lorebase_core-0.2.3 → lorebase_core-0.3.0}/tests/model/test_paths.py +0 -0
- {lorebase_core-0.2.3 → lorebase_core-0.3.0}/tests/model/test_supply_example.py +0 -0
- {lorebase_core-0.2.3 → lorebase_core-0.3.0}/tests/model/yaml_support.py +0 -0
- {lorebase_core-0.2.3 → lorebase_core-0.3.0}/tests/query/__init__.py +0 -0
- {lorebase_core-0.2.3 → lorebase_core-0.3.0}/tests/query/test_resolve.py +0 -0
- {lorebase_core-0.2.3/tests/revision → lorebase_core-0.3.0/tests/reconcile}/__init__.py +0 -0
- {lorebase_core-0.2.3/tests/tree → lorebase_core-0.3.0/tests/revision}/__init__.py +0 -0
- {lorebase_core-0.2.3 → lorebase_core-0.3.0}/tests/revision/test_ancestry.py +0 -0
- {lorebase_core-0.2.3 → lorebase_core-0.3.0}/tests/revision/test_revision.py +0 -0
- {lorebase_core-0.2.3/tests/types → lorebase_core-0.3.0/tests/tree}/__init__.py +0 -0
- {lorebase_core-0.2.3 → lorebase_core-0.3.0}/tests/tree/conftest.py +0 -0
- {lorebase_core-0.2.3 → lorebase_core-0.3.0}/tests/tree/support.py +0 -0
- {lorebase_core-0.2.3 → lorebase_core-0.3.0}/tests/tree/test_diff.py +0 -0
- {lorebase_core-0.2.3 → lorebase_core-0.3.0}/tests/tree/test_hamt.py +0 -0
- {lorebase_core-0.2.3 → lorebase_core-0.3.0}/tests/tree/test_node.py +0 -0
- {lorebase_core-0.2.3/tests/validate → lorebase_core-0.3.0/tests/types}/__init__.py +0 -0
- {lorebase_core-0.2.3 → lorebase_core-0.3.0}/tests/types/strategies.py +0 -0
- {lorebase_core-0.2.3 → lorebase_core-0.3.0}/tests/types/test_casts.py +0 -0
- {lorebase_core-0.2.3 → lorebase_core-0.3.0}/tests/types/test_constraints.py +0 -0
- {lorebase_core-0.2.3 → lorebase_core-0.3.0}/tests/types/test_encoding.py +0 -0
- {lorebase_core-0.2.3 → lorebase_core-0.3.0}/tests/validate/data.py +0 -0
- {lorebase_core-0.2.3 → lorebase_core-0.3.0}/tests/validate/test_changeset.py +0 -0
- {lorebase_core-0.2.3 → lorebase_core-0.3.0}/tests/validate/test_data_rules.py +0 -0
- {lorebase_core-0.2.3 → lorebase_core-0.3.0}/tests/validate/test_vault_rules.py +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.3.0"
|
|
@@ -43,6 +43,7 @@ from lorebase_core.model.derivation import (
|
|
|
43
43
|
PathDerivation,
|
|
44
44
|
)
|
|
45
45
|
from lorebase_core.model.kind import Kind
|
|
46
|
+
from lorebase_core.model.lens import Lens
|
|
46
47
|
from lorebase_core.model.operation import CreateLinkStep, DeleteLinkStep, Operation
|
|
47
48
|
from lorebase_core.model.relation import Relation
|
|
48
49
|
from lorebase_core.model.schema import Schema
|
|
@@ -126,10 +127,32 @@ def visible_schema(
|
|
|
126
127
|
d: schema.derivations[d] for d in schema.derivations if d in derivations
|
|
127
128
|
},
|
|
128
129
|
"sources": {s: schema.sources[s] for s in schema.sources if s in sources},
|
|
130
|
+
"lenses": _lenses(schema, shown, traits, relations | paths),
|
|
129
131
|
}
|
|
130
132
|
)
|
|
131
133
|
|
|
132
134
|
|
|
135
|
+
def _lenses(
|
|
136
|
+
schema: Schema, kinds: Collection[str], traits: Collection[str], relations: Collection[str]
|
|
137
|
+
) -> dict[str, Lens]:
|
|
138
|
+
"""The lenses as the reader sees them: members left out of the projection are dropped; a
|
|
139
|
+
lens that named Kinds or Traits and keeps none answers like an unknown one (design 8.8)."""
|
|
140
|
+
kept: dict[str, Lens] = {}
|
|
141
|
+
for lens_id, lens in schema.lenses.items():
|
|
142
|
+
shown_kinds = tuple(k for k in lens.kinds if k in kinds)
|
|
143
|
+
shown_traits = tuple(t for t in lens.traits if t in traits)
|
|
144
|
+
if (lens.kinds or lens.traits) and not (shown_kinds or shown_traits):
|
|
145
|
+
continue
|
|
146
|
+
kept[lens_id] = lens.model_copy(
|
|
147
|
+
update={
|
|
148
|
+
"kinds": shown_kinds,
|
|
149
|
+
"traits": shown_traits,
|
|
150
|
+
"relations": tuple(r for r in lens.relations if r in relations),
|
|
151
|
+
}
|
|
152
|
+
)
|
|
153
|
+
return kept
|
|
154
|
+
|
|
155
|
+
|
|
133
156
|
def _project(
|
|
134
157
|
schema: Schema, shown: Collection[str], limits: ExprLimits
|
|
135
158
|
) -> tuple[set[str], set[str], set[str], set[str]]:
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""The compared form of words, values and names (design 8.9).
|
|
2
|
+
|
|
3
|
+
Resolution, the value index and vocabulary suggestions compare text in one form: lower case
|
|
4
|
+
(final sigma folded), Latin combining marks dropped after NFD, blanks collapsed. The value
|
|
5
|
+
index computes the same form in SQL (`lb_value_norm`, revision 0045); `normalize` is its Python
|
|
6
|
+
twin, so both sides of a comparison agree.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import re
|
|
12
|
+
import unicodedata
|
|
13
|
+
|
|
14
|
+
_COMBINING = re.compile(f"[{chr(0x300)}-{chr(0x36F)}]")
|
|
15
|
+
BLANKS = "".join(
|
|
16
|
+
chr(c)
|
|
17
|
+
for c in (
|
|
18
|
+
*range(0x09, 0x0E), *range(0x1C, 0x21), 0x85, 0xA0, 0x1680, *range(0x2000, 0x200B),
|
|
19
|
+
0x2028, 0x2029, 0x202F, 0x205F, 0x3000,
|
|
20
|
+
)
|
|
21
|
+
) # fmt: skip
|
|
22
|
+
"""The blanks `lb_value_norm` collapses: what `str.split()` takes for white space."""
|
|
23
|
+
_BLANKS = re.compile(f"[{re.escape(BLANKS)}]+")
|
|
24
|
+
_CAMEL = re.compile(r"(?<=[a-z0-9])(?=[A-Z])")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def normalize(value: str) -> str:
|
|
28
|
+
"""The compared form of a value or a word, as `lb_value_norm` computes it: lower case
|
|
29
|
+
(final sigma folded), Latin combining marks dropped after NFD, blanks collapsed."""
|
|
30
|
+
lowered = value.lower().replace(chr(0x3C2), chr(0x3C3)) # final sigma
|
|
31
|
+
decomposed = unicodedata.normalize("NFD", lowered)
|
|
32
|
+
return _BLANKS.sub(" ", _COMBINING.sub("", decomposed)).strip(" ")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def name_words(api_name: str) -> str:
|
|
36
|
+
"""An api name as words: `unit_price` and `UnitPrice` give `unit price`."""
|
|
37
|
+
return normalize(_CAMEL.sub(" ", api_name).replace("_", " ").replace(".", " "))
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
__all__ = ["BLANKS", "name_words", "normalize"]
|
|
@@ -20,6 +20,16 @@ This module holds the pure parts:
|
|
|
20
20
|
activates them; Attributes added to an existing Kind are active.
|
|
21
21
|
|
|
22
22
|
A Kind proposed by another suggestion is referred to as ``+<KindName>`` until it exists.
|
|
23
|
+
|
|
24
|
+
**Vocabulary suggestions** (design 8.10) change elements that exist instead of adding new ones:
|
|
25
|
+
an alias of a Kind (``kind_alias``), of an Attribute (``attribute_alias``) or of a stored value
|
|
26
|
+
(``value_alias``), or a new description of an Attribute (``attribute_description``). Scribe's
|
|
27
|
+
consolidation pass proposes them from the words agents asked `lorebase_resolve` for that matched
|
|
28
|
+
nothing. ``name`` is the alias as compared (``lorebase_core.infer.phrases.normalize``), ``kind``
|
|
29
|
+
the Kind id, ``target`` the Attribute id, and ``payload`` holds what else the change needs (the
|
|
30
|
+
stored ``value`` an alias stands for, the proposed ``description``). They are never proposed or
|
|
31
|
+
applied for Attributes whose values are sensitive, kept in the vault or not resolvable
|
|
32
|
+
(``vocabulary_open``).
|
|
23
33
|
"""
|
|
24
34
|
|
|
25
35
|
from __future__ import annotations
|
|
@@ -37,8 +47,10 @@ from lorebase_core.expr.sensitivity import effective_tags
|
|
|
37
47
|
from lorebase_core.infer.columns import analyze_column, shape_pattern
|
|
38
48
|
from lorebase_core.infer.names import humanize, normalize, pascal, plural, singular, unique_name
|
|
39
49
|
from lorebase_core.infer.options import InferenceOptions
|
|
50
|
+
from lorebase_core.infer.phrases import name_words as phrase_of_name
|
|
51
|
+
from lorebase_core.infer.phrases import normalize as phrase_form
|
|
40
52
|
from lorebase_core.infer.profile import ColumnProfile
|
|
41
|
-
from lorebase_core.model.attribute import Attribute, Sensitivity
|
|
53
|
+
from lorebase_core.model.attribute import MAX_ALIASES, Attribute, Sensitivity
|
|
42
54
|
from lorebase_core.model.base import ElementStatus, snake_case
|
|
43
55
|
from lorebase_core.model.ids import IdPrefix, new_id
|
|
44
56
|
from lorebase_core.model.kind import Kind
|
|
@@ -55,6 +67,30 @@ class SuggestionType(StrEnum):
|
|
|
55
67
|
KIND = "kind"
|
|
56
68
|
ATTRIBUTE = "attribute"
|
|
57
69
|
RELATION = "relation"
|
|
70
|
+
KIND_ALIAS = "kind_alias"
|
|
71
|
+
ATTRIBUTE_ALIAS = "attribute_alias"
|
|
72
|
+
VALUE_ALIAS = "value_alias"
|
|
73
|
+
ATTRIBUTE_DESCRIPTION = "attribute_description"
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
VOCABULARY = frozenset(
|
|
77
|
+
{
|
|
78
|
+
SuggestionType.KIND_ALIAS,
|
|
79
|
+
SuggestionType.ATTRIBUTE_ALIAS,
|
|
80
|
+
SuggestionType.VALUE_ALIAS,
|
|
81
|
+
SuggestionType.ATTRIBUTE_DESCRIPTION,
|
|
82
|
+
}
|
|
83
|
+
)
|
|
84
|
+
"""Suggestion types that change existing elements (module doc)."""
|
|
85
|
+
_ORDER = (
|
|
86
|
+
SuggestionType.KIND,
|
|
87
|
+
SuggestionType.ATTRIBUTE,
|
|
88
|
+
SuggestionType.RELATION,
|
|
89
|
+
SuggestionType.KIND_ALIAS,
|
|
90
|
+
SuggestionType.ATTRIBUTE_ALIAS,
|
|
91
|
+
SuggestionType.VALUE_ALIAS,
|
|
92
|
+
SuggestionType.ATTRIBUTE_DESCRIPTION,
|
|
93
|
+
)
|
|
58
94
|
|
|
59
95
|
|
|
60
96
|
def _type_inference() -> InferenceOptions:
|
|
@@ -86,7 +122,15 @@ class SuggestionOptions(BaseModel):
|
|
|
86
122
|
max_tracked: int = Field(
|
|
87
123
|
default=2000,
|
|
88
124
|
ge=1,
|
|
89
|
-
description="Suggestions kept per Domain; observations of new
|
|
125
|
+
description="Suggestions from document extraction kept per Domain; observations of new "
|
|
126
|
+
"ones beyond are dropped.",
|
|
127
|
+
)
|
|
128
|
+
max_vocabulary_tracked: int = Field(
|
|
129
|
+
default=500,
|
|
130
|
+
ge=1,
|
|
131
|
+
description="Pending vocabulary suggestions (from resolution misses) kept per Domain, "
|
|
132
|
+
"counted apart from extraction's so that neither crowds out the other; new ones "
|
|
133
|
+
"beyond are dropped.",
|
|
90
134
|
)
|
|
91
135
|
name_similarity: float = Field(
|
|
92
136
|
default=0.85,
|
|
@@ -191,6 +235,10 @@ class Observation:
|
|
|
191
235
|
``kind`` is the owner Kind of an Attribute or the from Kind of a Relation, ``target`` the
|
|
192
236
|
to Kind of a Relation: a Kind id, or ``+<KindName>`` for a proposed Kind. ``value`` is a
|
|
193
237
|
sample value (an Attribute value, the key or name of a thing of a new Kind).
|
|
238
|
+
|
|
239
|
+
For vocabulary types (module doc) ``label`` is the alias as compared, ``kind`` the Kind id,
|
|
240
|
+
``target`` the Attribute id and ``payload`` what the change needs. ``weight`` is how many
|
|
241
|
+
sightings the observation stands for (the misses of a phrase).
|
|
194
242
|
"""
|
|
195
243
|
|
|
196
244
|
type: SuggestionType
|
|
@@ -202,25 +250,45 @@ class Observation:
|
|
|
202
250
|
target: str | None = None
|
|
203
251
|
value: str | None = None
|
|
204
252
|
evidence: Mapping[str, Any] | None = None
|
|
253
|
+
payload: Mapping[str, Any] | None = None
|
|
254
|
+
weight: int = 1
|
|
205
255
|
|
|
206
256
|
@property
|
|
207
257
|
def name(self) -> str:
|
|
258
|
+
if self.type in VOCABULARY:
|
|
259
|
+
return self.label
|
|
208
260
|
return kind_name(self.label) if self.type is SuggestionType.KIND else field_name(self.label)
|
|
209
261
|
|
|
210
262
|
@property
|
|
211
263
|
def fingerprint(self) -> str:
|
|
212
|
-
|
|
264
|
+
value = (self.payload or {}).get("value")
|
|
265
|
+
return fingerprint(
|
|
266
|
+
self.type, self.name, self.kind, self.target, value if isinstance(value, str) else None
|
|
267
|
+
)
|
|
213
268
|
|
|
214
269
|
|
|
215
270
|
def fingerprint(
|
|
216
|
-
type_: SuggestionType,
|
|
271
|
+
type_: SuggestionType,
|
|
272
|
+
name: str,
|
|
273
|
+
kind: str | None = None,
|
|
274
|
+
target: str | None = None,
|
|
275
|
+
value: str | None = None,
|
|
217
276
|
) -> str:
|
|
218
|
-
"""Identity of a suggestion: observations with the same fingerprint aggregate.
|
|
277
|
+
"""Identity of a suggestion: observations with the same fingerprint aggregate. A
|
|
278
|
+
vocabulary suggestion is one per alias and element (per Attribute for a description)."""
|
|
219
279
|
if type_ is SuggestionType.KIND:
|
|
220
280
|
return f"kind:{_canonical(name)}"
|
|
221
281
|
if type_ is SuggestionType.ATTRIBUTE:
|
|
222
282
|
return f"attribute:{kind}:{_canonical(name)}"
|
|
223
|
-
|
|
283
|
+
if type_ is SuggestionType.RELATION:
|
|
284
|
+
return f"relation:{kind}:{_canonical(name)}:{target}"
|
|
285
|
+
if type_ is SuggestionType.KIND_ALIAS:
|
|
286
|
+
return f"kind_alias:{kind}:{phrase_form(name)}"
|
|
287
|
+
if type_ is SuggestionType.ATTRIBUTE_ALIAS:
|
|
288
|
+
return f"attribute_alias:{kind}:{target}:{phrase_form(name)}"
|
|
289
|
+
if type_ is SuggestionType.VALUE_ALIAS:
|
|
290
|
+
return f"value_alias:{kind}:{target}:{value}:{phrase_form(name)}"
|
|
291
|
+
return f"attribute_description:{kind}:{target}"
|
|
224
292
|
|
|
225
293
|
|
|
226
294
|
# Deduplication ----------------------------------------------------------------------------------
|
|
@@ -453,6 +521,7 @@ def existing_match(
|
|
|
453
521
|
*,
|
|
454
522
|
kind: str | None = None,
|
|
455
523
|
target: str | None = None,
|
|
524
|
+
value: str | None = None,
|
|
456
525
|
options: SuggestionOptions,
|
|
457
526
|
tags: Mapping[str, frozenset[Sensitivity]] | None = None,
|
|
458
527
|
) -> ExistingMatch | None:
|
|
@@ -463,8 +532,15 @@ def existing_match(
|
|
|
463
532
|
(a field naming another Kind refers to it). A Relation matches a Relation between its
|
|
464
533
|
Kinds, in either direction, by name or by the name of its far Kind. ``tags`` are the
|
|
465
534
|
sensitivity tags by Attribute id (``effective_tags`` of the schema by default).
|
|
535
|
+
|
|
536
|
+
An alias matches the element whose name or alias it is already: any Kind for a Kind
|
|
537
|
+
alias, any Attribute of the Kind for an Attribute alias, the Attribute holding it as a
|
|
538
|
+
stored value's alias (or that value itself) for a value alias (``value``: the stored
|
|
539
|
+
value it would stand for). A description never duplicates.
|
|
466
540
|
"""
|
|
467
541
|
tags = effective_tags(schema) if tags is None else tags
|
|
542
|
+
if type_ in VOCABULARY:
|
|
543
|
+
return _vocabulary_match(schema, type_, name, kind, target, tags)
|
|
468
544
|
if type_ is SuggestionType.KIND:
|
|
469
545
|
return _best_kind(schema, name, options)
|
|
470
546
|
owner = resolve_kind(schema, kind, options)
|
|
@@ -506,6 +582,104 @@ def sensitive_attributes(schema: Schema) -> dict[str, frozenset[Sensitivity]]:
|
|
|
506
582
|
return {k: v for k, v in effective_tags(schema).items() if v}
|
|
507
583
|
|
|
508
584
|
|
|
585
|
+
# Vocabulary -------------------------------------------------------------------------------------
|
|
586
|
+
|
|
587
|
+
|
|
588
|
+
def kind_phrases(kind: Kind) -> frozenset[str]:
|
|
589
|
+
"""The compared forms that name a Kind: api name as words, display name, aliases."""
|
|
590
|
+
return frozenset(
|
|
591
|
+
p
|
|
592
|
+
for p in (
|
|
593
|
+
phrase_of_name(kind.api_name),
|
|
594
|
+
phrase_form(kind.display_name),
|
|
595
|
+
*(phrase_form(a) for a in kind.aliases or ()),
|
|
596
|
+
)
|
|
597
|
+
if p
|
|
598
|
+
)
|
|
599
|
+
|
|
600
|
+
|
|
601
|
+
def attribute_phrases(attribute: Attribute) -> frozenset[str]:
|
|
602
|
+
"""The compared forms that name an Attribute: api name as words, display name, aliases."""
|
|
603
|
+
return frozenset(
|
|
604
|
+
p
|
|
605
|
+
for p in (
|
|
606
|
+
phrase_of_name(attribute.api_name),
|
|
607
|
+
phrase_form(attribute.display_name),
|
|
608
|
+
*(phrase_form(a) for a in attribute.aliases or ()),
|
|
609
|
+
)
|
|
610
|
+
if p
|
|
611
|
+
)
|
|
612
|
+
|
|
613
|
+
|
|
614
|
+
def value_phrases(attribute: Attribute) -> frozenset[str]:
|
|
615
|
+
"""The compared forms of the values the schema names for an Attribute: enum values,
|
|
616
|
+
values that carry aliases, and those aliases."""
|
|
617
|
+
found: set[str] = set()
|
|
618
|
+
spec = attribute.type.element if attribute.type.name is AttributeType.ARRAY else attribute.type
|
|
619
|
+
if spec is not None:
|
|
620
|
+
found.update(phrase_form(v) for v in spec.enum_values or ())
|
|
621
|
+
for stored, aliases in (attribute.value_aliases or {}).items():
|
|
622
|
+
found.add(phrase_form(stored))
|
|
623
|
+
found.update(phrase_form(a) for a in aliases)
|
|
624
|
+
return frozenset(p for p in found if p)
|
|
625
|
+
|
|
626
|
+
|
|
627
|
+
def vocabulary_open(
|
|
628
|
+
attribute: Attribute, tags: frozenset[Sensitivity] = frozenset(), *, values: bool = False
|
|
629
|
+
) -> bool:
|
|
630
|
+
"""Whether aliases or a description may be proposed for ``attribute``: it carries no
|
|
631
|
+
sensitivity tag (``tags``: its effective tags, through Derivations), is not kept in the
|
|
632
|
+
vault, not derived and not marked ``resolvable: false``; with ``values``, its values are
|
|
633
|
+
strings or enums (value aliases)."""
|
|
634
|
+
if tags or attribute.sensitivity or attribute.in_vault or attribute.is_derived:
|
|
635
|
+
return False
|
|
636
|
+
if attribute.resolvable is False:
|
|
637
|
+
return False
|
|
638
|
+
return attribute.value_resolvable or not values
|
|
639
|
+
|
|
640
|
+
|
|
641
|
+
def _vocabulary_match(
|
|
642
|
+
schema: Schema,
|
|
643
|
+
type_: SuggestionType,
|
|
644
|
+
name: str,
|
|
645
|
+
kind: str | None,
|
|
646
|
+
target: str | None,
|
|
647
|
+
tags: Mapping[str, frozenset[Sensitivity]],
|
|
648
|
+
) -> ExistingMatch | None:
|
|
649
|
+
owner = schema.kinds.get(kind or "")
|
|
650
|
+
phrase = phrase_form(name)
|
|
651
|
+
if owner is None or type_ is SuggestionType.ATTRIBUTE_DESCRIPTION:
|
|
652
|
+
return None
|
|
653
|
+
if type_ is SuggestionType.KIND_ALIAS:
|
|
654
|
+
for other in schema.kinds.values():
|
|
655
|
+
if phrase in kind_phrases(other):
|
|
656
|
+
return ExistingMatch("kinds", other.id, other.api_name, 1.0)
|
|
657
|
+
return None
|
|
658
|
+
attribute = owner.attributes.get(target or "")
|
|
659
|
+
if attribute is None:
|
|
660
|
+
return None
|
|
661
|
+
if type_ is SuggestionType.ATTRIBUTE_ALIAS:
|
|
662
|
+
for sibling in owner.attributes.values():
|
|
663
|
+
if phrase in attribute_phrases(sibling):
|
|
664
|
+
return ExistingMatch(
|
|
665
|
+
"attributes",
|
|
666
|
+
sibling.id,
|
|
667
|
+
sibling.api_name,
|
|
668
|
+
1.0,
|
|
669
|
+
tags.get(sibling.id, frozenset()),
|
|
670
|
+
)
|
|
671
|
+
return None
|
|
672
|
+
if phrase in value_phrases(attribute):
|
|
673
|
+
return ExistingMatch(
|
|
674
|
+
"attributes",
|
|
675
|
+
attribute.id,
|
|
676
|
+
attribute.api_name,
|
|
677
|
+
1.0,
|
|
678
|
+
tags.get(attribute.id, frozenset()),
|
|
679
|
+
)
|
|
680
|
+
return None
|
|
681
|
+
|
|
682
|
+
|
|
509
683
|
# Types ------------------------------------------------------------------------------------------
|
|
510
684
|
|
|
511
685
|
|
|
@@ -543,6 +717,7 @@ class Suggestion:
|
|
|
543
717
|
target: str | None = None
|
|
544
718
|
samples: tuple[str, ...] = ()
|
|
545
719
|
labels: tuple[str, ...] = ()
|
|
720
|
+
payload: Mapping[str, Any] = field(default_factory=dict)
|
|
546
721
|
|
|
547
722
|
|
|
548
723
|
class SuggestionError(ValueError):
|
|
@@ -632,6 +807,92 @@ def _relation_names(schema: Schema, extra: Iterable[Relation]) -> set[str]:
|
|
|
632
807
|
return names
|
|
633
808
|
|
|
634
809
|
|
|
810
|
+
def _with_alias(
|
|
811
|
+
aliases: tuple[str, ...] | None, alias: str, suggestion: Suggestion
|
|
812
|
+
) -> tuple[str, ...]:
|
|
813
|
+
kept = tuple(aliases or ())
|
|
814
|
+
if alias in kept:
|
|
815
|
+
return kept
|
|
816
|
+
if len(kept) >= MAX_ALIASES:
|
|
817
|
+
raise SuggestionError(suggestion.id, f"the element has {MAX_ALIASES} aliases already")
|
|
818
|
+
return (*kept, alias)
|
|
819
|
+
|
|
820
|
+
|
|
821
|
+
def _draft_phrases(draft: _Draft) -> dict[str, str]:
|
|
822
|
+
"""Every name and alias of the draft schema (the schema with the changes applied so far),
|
|
823
|
+
compared form -> what it names."""
|
|
824
|
+
kinds = {k.api_name: k for k in draft.schema.kinds.values()}
|
|
825
|
+
kinds.update(draft.kinds)
|
|
826
|
+
taken: dict[str, str] = {}
|
|
827
|
+
for kind in kinds.values():
|
|
828
|
+
for phrase in kind_phrases(kind):
|
|
829
|
+
taken.setdefault(phrase, f"Kind {kind.api_name}")
|
|
830
|
+
for attribute in kind.attributes.values():
|
|
831
|
+
where = f"{kind.api_name}.{attribute.api_name}"
|
|
832
|
+
for phrase in attribute_phrases(attribute):
|
|
833
|
+
taken.setdefault(phrase, f"Attribute {where}")
|
|
834
|
+
for phrase in value_phrases(attribute):
|
|
835
|
+
taken.setdefault(phrase, f"a value of {where}")
|
|
836
|
+
return taken
|
|
837
|
+
|
|
838
|
+
|
|
839
|
+
def _vocabulary_change(
|
|
840
|
+
draft: _Draft, suggestion: Suggestion, tags: Mapping[str, frozenset[Sensitivity]]
|
|
841
|
+
) -> None:
|
|
842
|
+
"""Apply an alias or description suggestion to the draft copy of its Kind. An alias
|
|
843
|
+
that already names anything in the draft (a Kind, an Attribute, a value or one of their
|
|
844
|
+
aliases, the suggestions accepted with it included) is refused: it would be ambiguous."""
|
|
845
|
+
owner = draft.kind(suggestion.kind, suggestion)
|
|
846
|
+
alias = phrase_form(suggestion.name)
|
|
847
|
+
if suggestion.type is not SuggestionType.ATTRIBUTE_DESCRIPTION:
|
|
848
|
+
named = _draft_phrases(draft).get(alias)
|
|
849
|
+
if named is not None:
|
|
850
|
+
raise SuggestionError(suggestion.id, f"{alias!r} already names {named}")
|
|
851
|
+
if suggestion.type is SuggestionType.KIND_ALIAS:
|
|
852
|
+
if not alias:
|
|
853
|
+
raise SuggestionError(suggestion.id, "the alias is empty")
|
|
854
|
+
changed = owner.model_copy(
|
|
855
|
+
update={"aliases": _with_alias(owner.aliases, alias, suggestion)}
|
|
856
|
+
)
|
|
857
|
+
draft.kinds[owner.api_name] = changed
|
|
858
|
+
draft.touched_kinds.add(owner.api_name)
|
|
859
|
+
return
|
|
860
|
+
attribute = owner.attributes.get(suggestion.target or "")
|
|
861
|
+
if attribute is None:
|
|
862
|
+
raise SuggestionError(
|
|
863
|
+
suggestion.id, f"Kind {owner.api_name!r} has no Attribute {suggestion.target!r}"
|
|
864
|
+
)
|
|
865
|
+
values = suggestion.type is SuggestionType.VALUE_ALIAS
|
|
866
|
+
if not vocabulary_open(attribute, tags.get(attribute.id, frozenset()), values=values):
|
|
867
|
+
raise SuggestionError(
|
|
868
|
+
suggestion.id,
|
|
869
|
+
f"Attribute {attribute.api_name!r} is sensitive, kept in the vault, derived or not "
|
|
870
|
+
"resolvable: no alias or description is proposed for it",
|
|
871
|
+
)
|
|
872
|
+
update: dict[str, Any]
|
|
873
|
+
if suggestion.type is SuggestionType.ATTRIBUTE_DESCRIPTION:
|
|
874
|
+
description = suggestion.payload.get("description")
|
|
875
|
+
if not isinstance(description, str) or not description.strip():
|
|
876
|
+
raise SuggestionError(suggestion.id, "the suggestion holds no description")
|
|
877
|
+
update = {"description": description.strip()}
|
|
878
|
+
elif suggestion.type is SuggestionType.ATTRIBUTE_ALIAS:
|
|
879
|
+
if not alias:
|
|
880
|
+
raise SuggestionError(suggestion.id, "the alias is empty")
|
|
881
|
+
update = {"aliases": _with_alias(attribute.aliases, alias, suggestion)}
|
|
882
|
+
else:
|
|
883
|
+
value = suggestion.payload.get("value")
|
|
884
|
+
if not isinstance(value, str) or not value or not alias:
|
|
885
|
+
raise SuggestionError(suggestion.id, "the suggestion names no stored value")
|
|
886
|
+
current = dict(attribute.value_aliases or {})
|
|
887
|
+
current[value] = _with_alias(current.get(value), alias, suggestion)
|
|
888
|
+
update = {"value_aliases": current}
|
|
889
|
+
changed_attribute = attribute.model_copy(update=update)
|
|
890
|
+
draft.kinds[owner.api_name] = owner.model_copy(
|
|
891
|
+
update={"attributes": {**owner.attributes, attribute.id: changed_attribute}}
|
|
892
|
+
)
|
|
893
|
+
draft.touched_kinds.add(owner.api_name)
|
|
894
|
+
|
|
895
|
+
|
|
635
896
|
def suggestion_patch(
|
|
636
897
|
schema: Schema, suggestions: Sequence[Suggestion], options: SuggestionOptions
|
|
637
898
|
) -> dict[str, Any]:
|
|
@@ -639,16 +900,12 @@ def suggestion_patch(
|
|
|
639
900
|
``relations`` sections holding the new and the changed elements (whole, as the patch
|
|
640
901
|
replaces elements by api name). Raises ``SuggestionError``."""
|
|
641
902
|
draft = _Draft(schema, options)
|
|
642
|
-
ordered = sorted(
|
|
643
|
-
|
|
644
|
-
key=lambda s: [
|
|
645
|
-
SuggestionType.KIND,
|
|
646
|
-
SuggestionType.ATTRIBUTE,
|
|
647
|
-
SuggestionType.RELATION,
|
|
648
|
-
].index(s.type),
|
|
649
|
-
)
|
|
903
|
+
ordered = sorted(suggestions, key=lambda s: _ORDER.index(s.type))
|
|
904
|
+
tags = effective_tags(schema)
|
|
650
905
|
for suggestion in ordered:
|
|
651
|
-
if suggestion.type
|
|
906
|
+
if suggestion.type in VOCABULARY:
|
|
907
|
+
_vocabulary_change(draft, suggestion, tags)
|
|
908
|
+
elif suggestion.type is SuggestionType.KIND:
|
|
652
909
|
if schema.kind_by_api_name(suggestion.name) or suggestion.name in draft.kinds:
|
|
653
910
|
raise SuggestionError(suggestion.id, f"Kind {suggestion.name!r} exists")
|
|
654
911
|
kind = _new_kind(suggestion, options)
|
|
@@ -703,6 +960,7 @@ def suggestion_patch(
|
|
|
703
960
|
|
|
704
961
|
__all__ = [
|
|
705
962
|
"PROPOSED",
|
|
963
|
+
"VOCABULARY",
|
|
706
964
|
"AttributeGuess",
|
|
707
965
|
"AttributeSighting",
|
|
708
966
|
"ExistingMatch",
|
|
@@ -711,6 +969,7 @@ __all__ = [
|
|
|
711
969
|
"SuggestionError",
|
|
712
970
|
"SuggestionOptions",
|
|
713
971
|
"SuggestionType",
|
|
972
|
+
"attribute_phrases",
|
|
714
973
|
"dangling_reference_names",
|
|
715
974
|
"existing_match",
|
|
716
975
|
"field_name",
|
|
@@ -720,6 +979,7 @@ __all__ = [
|
|
|
720
979
|
"kind_is_independent",
|
|
721
980
|
"kind_is_noise",
|
|
722
981
|
"kind_name",
|
|
982
|
+
"kind_phrases",
|
|
723
983
|
"merge_attributes",
|
|
724
984
|
"name_similarity",
|
|
725
985
|
"proposed_kind",
|
|
@@ -727,5 +987,7 @@ __all__ = [
|
|
|
727
987
|
"sensitive_attributes",
|
|
728
988
|
"suggestion_patch",
|
|
729
989
|
"value_classes",
|
|
990
|
+
"value_phrases",
|
|
991
|
+
"vocabulary_open",
|
|
730
992
|
"word_overlap",
|
|
731
993
|
]
|
|
@@ -46,6 +46,7 @@ from lorebase_core.merge.policies import (
|
|
|
46
46
|
PolicyName,
|
|
47
47
|
origin_labels,
|
|
48
48
|
)
|
|
49
|
+
from lorebase_core.merge.text_merge import TextGranularity
|
|
49
50
|
|
|
50
51
|
__all__ = [
|
|
51
52
|
"CHOICES",
|
|
@@ -76,6 +77,7 @@ __all__ = [
|
|
|
76
77
|
"ResolutionError",
|
|
77
78
|
"SchemaLoader",
|
|
78
79
|
"Side",
|
|
80
|
+
"TextGranularity",
|
|
79
81
|
"TreeIndexView",
|
|
80
82
|
"UnresolvedConflictsError",
|
|
81
83
|
"apply_resolutions",
|