cognite-neat 0.123.24__py3-none-any.whl → 1.0.22__py3-none-any.whl
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.
- cognite/neat/__init__.py +4 -3
- cognite/neat/_client/__init__.py +5 -0
- cognite/neat/_client/api.py +8 -0
- cognite/neat/_client/client.py +21 -0
- cognite/neat/_client/config.py +40 -0
- cognite/neat/_client/containers_api.py +138 -0
- cognite/neat/_client/data_classes.py +44 -0
- cognite/neat/_client/data_model_api.py +115 -0
- cognite/neat/_client/init/credentials.py +70 -0
- cognite/neat/_client/init/env_vars.py +131 -0
- cognite/neat/_client/init/main.py +51 -0
- cognite/neat/_client/spaces_api.py +115 -0
- cognite/neat/_client/statistics_api.py +24 -0
- cognite/neat/_client/views_api.py +144 -0
- cognite/neat/_config.py +266 -0
- cognite/neat/_data_model/_analysis.py +571 -0
- cognite/neat/_data_model/_constants.py +74 -0
- cognite/neat/_data_model/_identifiers.py +61 -0
- cognite/neat/_data_model/_shared.py +41 -0
- cognite/neat/_data_model/_snapshot.py +134 -0
- cognite/neat/_data_model/deployer/_differ.py +140 -0
- cognite/neat/_data_model/deployer/_differ_container.py +360 -0
- cognite/neat/_data_model/deployer/_differ_data_model.py +54 -0
- cognite/neat/_data_model/deployer/_differ_space.py +9 -0
- cognite/neat/_data_model/deployer/_differ_view.py +299 -0
- cognite/neat/_data_model/deployer/data_classes.py +644 -0
- cognite/neat/_data_model/deployer/deployer.py +431 -0
- cognite/neat/_data_model/exporters/__init__.py +15 -0
- cognite/neat/_data_model/exporters/_api_exporter.py +37 -0
- cognite/neat/_data_model/exporters/_base.py +24 -0
- cognite/neat/_data_model/exporters/_table_exporter/exporter.py +128 -0
- cognite/neat/_data_model/exporters/_table_exporter/workbook.py +409 -0
- cognite/neat/_data_model/exporters/_table_exporter/writer.py +480 -0
- cognite/neat/_data_model/importers/__init__.py +5 -0
- cognite/neat/_data_model/importers/_api_importer.py +166 -0
- cognite/neat/_data_model/importers/_base.py +16 -0
- cognite/neat/_data_model/importers/_table_importer/data_classes.py +344 -0
- cognite/neat/_data_model/importers/_table_importer/importer.py +192 -0
- cognite/neat/_data_model/importers/_table_importer/reader.py +1102 -0
- cognite/neat/_data_model/importers/_table_importer/source.py +94 -0
- cognite/neat/_data_model/models/conceptual/_base.py +18 -0
- cognite/neat/_data_model/models/conceptual/_concept.py +67 -0
- cognite/neat/_data_model/models/conceptual/_data_model.py +51 -0
- cognite/neat/_data_model/models/conceptual/_properties.py +104 -0
- cognite/neat/_data_model/models/conceptual/_property.py +105 -0
- cognite/neat/_data_model/models/dms/__init__.py +206 -0
- cognite/neat/_data_model/models/dms/_base.py +31 -0
- cognite/neat/_data_model/models/dms/_constants.py +48 -0
- cognite/neat/_data_model/models/dms/_constraints.py +42 -0
- cognite/neat/_data_model/models/dms/_container.py +159 -0
- cognite/neat/_data_model/models/dms/_data_model.py +95 -0
- cognite/neat/_data_model/models/dms/_data_types.py +195 -0
- cognite/neat/_data_model/models/dms/_http.py +28 -0
- cognite/neat/_data_model/models/dms/_indexes.py +30 -0
- cognite/neat/_data_model/models/dms/_limits.py +96 -0
- cognite/neat/_data_model/models/dms/_references.py +141 -0
- cognite/neat/_data_model/models/dms/_schema.py +18 -0
- cognite/neat/_data_model/models/dms/_space.py +48 -0
- cognite/neat/_data_model/models/dms/_types.py +17 -0
- cognite/neat/_data_model/models/dms/_view_filter.py +310 -0
- cognite/neat/_data_model/models/dms/_view_property.py +235 -0
- cognite/neat/_data_model/models/dms/_views.py +216 -0
- cognite/neat/_data_model/models/entities/__init__.py +50 -0
- cognite/neat/_data_model/models/entities/_base.py +101 -0
- cognite/neat/_data_model/models/entities/_constants.py +22 -0
- cognite/neat/_data_model/models/entities/_data_types.py +144 -0
- cognite/neat/_data_model/models/entities/_identifiers.py +61 -0
- cognite/neat/_data_model/models/entities/_parser.py +226 -0
- cognite/neat/_data_model/validation/dms/__init__.py +75 -0
- cognite/neat/_data_model/validation/dms/_ai_readiness.py +381 -0
- cognite/neat/_data_model/validation/dms/_base.py +25 -0
- cognite/neat/_data_model/validation/dms/_connections.py +681 -0
- cognite/neat/_data_model/validation/dms/_consistency.py +58 -0
- cognite/neat/_data_model/validation/dms/_containers.py +199 -0
- cognite/neat/_data_model/validation/dms/_limits.py +368 -0
- cognite/neat/_data_model/validation/dms/_orchestrator.py +70 -0
- cognite/neat/_data_model/validation/dms/_views.py +164 -0
- cognite/neat/_exceptions.py +68 -0
- cognite/neat/_issues.py +68 -0
- cognite/neat/_session/__init__.py +3 -0
- cognite/neat/_session/_html/_render.py +30 -0
- cognite/neat/_session/_html/static/__init__.py +8 -0
- cognite/neat/_session/_html/static/deployment.css +476 -0
- cognite/neat/_session/_html/static/deployment.js +181 -0
- cognite/neat/_session/_html/static/issues.css +211 -0
- cognite/neat/_session/_html/static/issues.js +168 -0
- cognite/neat/_session/_html/static/shared.css +186 -0
- cognite/neat/_session/_html/templates/__init__.py +4 -0
- cognite/neat/_session/_html/templates/deployment.html +80 -0
- cognite/neat/_session/_html/templates/issues.html +45 -0
- cognite/neat/_session/_issues.py +81 -0
- cognite/neat/_session/_physical.py +294 -0
- cognite/neat/_session/_result/__init__.py +3 -0
- cognite/neat/_session/_result/_deployment/__init__.py +0 -0
- cognite/neat/_session/_result/_deployment/_physical/__init__.py +0 -0
- cognite/neat/_session/_result/_deployment/_physical/_changes.py +196 -0
- cognite/neat/_session/_result/_deployment/_physical/_statistics.py +180 -0
- cognite/neat/_session/_result/_deployment/_physical/serializer.py +35 -0
- cognite/neat/_session/_result/_result.py +31 -0
- cognite/neat/_session/_session.py +81 -0
- cognite/neat/_session/_usage_analytics/__init__.py +0 -0
- cognite/neat/_session/_usage_analytics/_collector.py +131 -0
- cognite/neat/_session/_usage_analytics/_constants.py +23 -0
- cognite/neat/_session/_usage_analytics/_storage.py +240 -0
- cognite/neat/_session/_wrappers.py +101 -0
- cognite/neat/_state_machine/__init__.py +10 -0
- cognite/neat/_state_machine/_base.py +37 -0
- cognite/neat/_state_machine/_states.py +52 -0
- cognite/neat/_store/__init__.py +3 -0
- cognite/neat/_store/_provenance.py +88 -0
- cognite/neat/_store/_store.py +220 -0
- cognite/neat/_utils/__init__.py +0 -0
- cognite/neat/_utils/_reader.py +194 -0
- cognite/neat/_utils/auxiliary.py +49 -0
- cognite/neat/_utils/collection.py +11 -0
- cognite/neat/_utils/http_client/__init__.py +39 -0
- cognite/neat/_utils/http_client/_client.py +245 -0
- cognite/neat/_utils/http_client/_config.py +19 -0
- cognite/neat/_utils/http_client/_data_classes.py +294 -0
- cognite/neat/_utils/http_client/_tracker.py +31 -0
- cognite/neat/_utils/repo.py +19 -0
- cognite/neat/_utils/text.py +71 -0
- cognite/neat/_utils/useful_types.py +37 -0
- cognite/neat/_utils/validation.py +154 -0
- cognite/neat/_v0/__init__.py +0 -0
- cognite/neat/_v0/core/__init__.py +0 -0
- cognite/neat/_v0/core/_client/_api/__init__.py +0 -0
- cognite/neat/{core → _v0/core}/_client/_api/data_modeling_loaders.py +8 -7
- cognite/neat/{core → _v0/core}/_client/_api/neat_instances.py +5 -5
- cognite/neat/{core → _v0/core}/_client/_api/schema.py +5 -5
- cognite/neat/{core → _v0/core}/_client/_api/statistics.py +3 -3
- cognite/neat/{core → _v0/core}/_client/_api_client.py +1 -1
- cognite/neat/_v0/core/_client/data_classes/__init__.py +0 -0
- cognite/neat/{core → _v0/core}/_client/data_classes/schema.py +4 -4
- cognite/neat/{core → _v0/core}/_client/testing.py +1 -1
- cognite/neat/{core → _v0/core}/_constants.py +5 -3
- cognite/neat/_v0/core/_data_model/__init__.py +0 -0
- cognite/neat/{core → _v0/core}/_data_model/_constants.py +7 -0
- cognite/neat/{core → _v0/core}/_data_model/_shared.py +4 -4
- cognite/neat/{core → _v0/core}/_data_model/analysis/_base.py +8 -8
- cognite/neat/{core → _v0/core}/_data_model/exporters/__init__.py +1 -2
- cognite/neat/{core → _v0/core}/_data_model/exporters/_base.py +7 -7
- cognite/neat/{core → _v0/core}/_data_model/exporters/_data_model2dms.py +9 -9
- cognite/neat/{core → _v0/core}/_data_model/exporters/_data_model2excel.py +12 -12
- cognite/neat/{core → _v0/core}/_data_model/exporters/_data_model2instance_template.py +4 -4
- cognite/neat/{core/_data_model/exporters/_data_model2ontology.py → _v0/core/_data_model/exporters/_data_model2semantic_model.py} +126 -116
- cognite/neat/{core → _v0/core}/_data_model/exporters/_data_model2yaml.py +1 -1
- cognite/neat/{core → _v0/core}/_data_model/importers/_base.py +5 -5
- cognite/neat/{core → _v0/core}/_data_model/importers/_base_file_reader.py +2 -2
- cognite/neat/{core → _v0/core}/_data_model/importers/_dict2data_model.py +5 -5
- cognite/neat/{core → _v0/core}/_data_model/importers/_dms2data_model.py +18 -15
- cognite/neat/{core → _v0/core}/_data_model/importers/_graph2data_model.py +12 -12
- cognite/neat/{core → _v0/core}/_data_model/importers/_rdf/_base.py +12 -12
- cognite/neat/{core → _v0/core}/_data_model/importers/_rdf/_inference2rdata_model.py +14 -14
- cognite/neat/_v0/core/_data_model/importers/_rdf/_owl2data_model.py +144 -0
- cognite/neat/{core → _v0/core}/_data_model/importers/_rdf/_shared.py +17 -13
- cognite/neat/{core → _v0/core}/_data_model/importers/_spreadsheet2data_model.py +92 -12
- cognite/neat/{core → _v0/core}/_data_model/models/__init__.py +3 -3
- cognite/neat/{core → _v0/core}/_data_model/models/_base_verified.py +5 -5
- cognite/neat/{core → _v0/core}/_data_model/models/_import_contexts.py +1 -1
- cognite/neat/{core → _v0/core}/_data_model/models/_types.py +5 -5
- cognite/neat/{core → _v0/core}/_data_model/models/conceptual/_unverified.py +16 -10
- cognite/neat/{core → _v0/core}/_data_model/models/conceptual/_validation.py +12 -12
- cognite/neat/{core → _v0/core}/_data_model/models/conceptual/_verified.py +9 -9
- cognite/neat/{core → _v0/core}/_data_model/models/data_types.py +14 -4
- cognite/neat/{core → _v0/core}/_data_model/models/entities/__init__.py +6 -0
- cognite/neat/_v0/core/_data_model/models/entities/_loaders.py +155 -0
- cognite/neat/{core → _v0/core}/_data_model/models/entities/_multi_value.py +2 -2
- cognite/neat/_v0/core/_data_model/models/entities/_restrictions.py +230 -0
- cognite/neat/{core → _v0/core}/_data_model/models/entities/_single_value.py +121 -16
- cognite/neat/{core → _v0/core}/_data_model/models/entities/_types.py +10 -0
- cognite/neat/{core → _v0/core}/_data_model/models/mapping/_classic2core.py +5 -5
- cognite/neat/{core → _v0/core}/_data_model/models/physical/__init__.py +1 -1
- cognite/neat/{core → _v0/core}/_data_model/models/physical/_exporter.py +26 -19
- cognite/neat/{core → _v0/core}/_data_model/models/physical/_unverified.py +133 -37
- cognite/neat/{core → _v0/core}/_data_model/models/physical/_validation.py +24 -20
- cognite/neat/{core → _v0/core}/_data_model/models/physical/_verified.py +95 -24
- cognite/neat/{core → _v0/core}/_data_model/transformers/_base.py +4 -4
- cognite/neat/{core → _v0/core}/_data_model/transformers/_converters.py +35 -28
- cognite/neat/{core → _v0/core}/_data_model/transformers/_mapping.py +7 -7
- cognite/neat/{core → _v0/core}/_data_model/transformers/_union_conceptual.py +5 -5
- cognite/neat/{core → _v0/core}/_data_model/transformers/_verification.py +7 -7
- cognite/neat/_v0/core/_instances/__init__.py +0 -0
- cognite/neat/{core → _v0/core}/_instances/_tracking/base.py +1 -1
- cognite/neat/{core → _v0/core}/_instances/_tracking/log.py +1 -1
- cognite/neat/{core → _v0/core}/_instances/extractors/__init__.py +3 -2
- cognite/neat/{core → _v0/core}/_instances/extractors/_base.py +6 -6
- cognite/neat/_v0/core/_instances/extractors/_classic_cdf/__init__.py +0 -0
- cognite/neat/{core → _v0/core}/_instances/extractors/_classic_cdf/_base.py +7 -7
- cognite/neat/{core → _v0/core}/_instances/extractors/_classic_cdf/_classic.py +12 -12
- cognite/neat/{core → _v0/core}/_instances/extractors/_classic_cdf/_relationships.py +3 -3
- cognite/neat/{core → _v0/core}/_instances/extractors/_classic_cdf/_sequences.py +2 -2
- cognite/neat/{core → _v0/core}/_instances/extractors/_dict.py +6 -3
- cognite/neat/{core → _v0/core}/_instances/extractors/_dms.py +6 -6
- cognite/neat/{core → _v0/core}/_instances/extractors/_dms_graph.py +11 -11
- cognite/neat/{core → _v0/core}/_instances/extractors/_mock_graph_generator.py +10 -10
- cognite/neat/{core → _v0/core}/_instances/extractors/_raw.py +3 -3
- cognite/neat/{core → _v0/core}/_instances/extractors/_rdf_file.py +7 -7
- cognite/neat/{core → _v0/core}/_instances/loaders/_base.py +5 -5
- cognite/neat/{core → _v0/core}/_instances/loaders/_rdf2dms.py +17 -17
- cognite/neat/{core → _v0/core}/_instances/loaders/_rdf_to_instance_space.py +11 -11
- cognite/neat/{core → _v0/core}/_instances/queries/_select.py +29 -3
- cognite/neat/{core → _v0/core}/_instances/queries/_update.py +1 -1
- cognite/neat/{core → _v0/core}/_instances/transformers/_base.py +4 -4
- cognite/neat/{core → _v0/core}/_instances/transformers/_classic_cdf.py +6 -6
- cognite/neat/{core → _v0/core}/_instances/transformers/_prune_graph.py +4 -4
- cognite/neat/{core → _v0/core}/_instances/transformers/_rdfpath.py +1 -1
- cognite/neat/{core → _v0/core}/_instances/transformers/_value_type.py +4 -4
- cognite/neat/{core → _v0/core}/_issues/_base.py +5 -5
- cognite/neat/{core → _v0/core}/_issues/_contextmanagers.py +1 -1
- cognite/neat/{core → _v0/core}/_issues/_factory.py +3 -3
- cognite/neat/{core → _v0/core}/_issues/errors/__init__.py +1 -1
- cognite/neat/{core → _v0/core}/_issues/errors/_external.py +1 -1
- cognite/neat/{core → _v0/core}/_issues/errors/_general.py +1 -1
- cognite/neat/{core → _v0/core}/_issues/errors/_properties.py +1 -1
- cognite/neat/{core → _v0/core}/_issues/errors/_resources.py +2 -2
- cognite/neat/{core → _v0/core}/_issues/errors/_wrapper.py +7 -3
- cognite/neat/{core → _v0/core}/_issues/warnings/__init__.py +1 -1
- cognite/neat/{core → _v0/core}/_issues/warnings/_external.py +1 -1
- cognite/neat/{core → _v0/core}/_issues/warnings/_general.py +1 -1
- cognite/neat/{core → _v0/core}/_issues/warnings/_models.py +2 -2
- cognite/neat/{core → _v0/core}/_issues/warnings/_properties.py +2 -2
- cognite/neat/{core → _v0/core}/_issues/warnings/_resources.py +1 -1
- cognite/neat/{core → _v0/core}/_issues/warnings/user_modeling.py +1 -1
- cognite/neat/{core → _v0/core}/_store/_data_model.py +12 -12
- cognite/neat/{core → _v0/core}/_store/_instance.py +43 -10
- cognite/neat/{core → _v0/core}/_store/_provenance.py +3 -3
- cognite/neat/{core → _v0/core}/_store/exceptions.py +4 -4
- cognite/neat/_v0/core/_utils/__init__.py +0 -0
- cognite/neat/{core → _v0/core}/_utils/auth.py +22 -12
- cognite/neat/{core → _v0/core}/_utils/auxiliary.py +1 -1
- cognite/neat/{core → _v0/core}/_utils/collection_.py +2 -2
- cognite/neat/{core → _v0/core}/_utils/graph_transformations_report.py +1 -1
- cognite/neat/{core → _v0/core}/_utils/rdf_.py +1 -1
- cognite/neat/{core → _v0/core}/_utils/reader/_base.py +1 -1
- cognite/neat/{core → _v0/core}/_utils/spreadsheet.py +18 -4
- cognite/neat/{core → _v0/core}/_utils/text.py +1 -1
- cognite/neat/{core → _v0/core}/_utils/upload.py +3 -3
- cognite/neat/{session → _v0}/engine/_load.py +1 -1
- cognite/neat/_v0/plugins/__init__.py +4 -0
- cognite/neat/_v0/plugins/_base.py +9 -0
- cognite/neat/_v0/plugins/_data_model.py +48 -0
- cognite/neat/{plugins → _v0/plugins}/_issues.py +1 -1
- cognite/neat/{plugins → _v0/plugins}/_manager.py +7 -16
- cognite/neat/{session → _v0/session}/_base.py +13 -15
- cognite/neat/{session → _v0/session}/_collector.py +1 -1
- cognite/neat/_v0/session/_diff.py +51 -0
- cognite/neat/{session → _v0/session}/_drop.py +3 -3
- cognite/neat/{session → _v0/session}/_explore.py +2 -2
- cognite/neat/{session → _v0/session}/_fix.py +2 -2
- cognite/neat/{session → _v0/session}/_inspect.py +3 -3
- cognite/neat/{session → _v0/session}/_mapping.py +3 -3
- cognite/neat/{session → _v0/session}/_plugin.py +4 -5
- cognite/neat/{session → _v0/session}/_prepare.py +8 -8
- cognite/neat/{session → _v0/session}/_read.py +34 -21
- cognite/neat/{session → _v0/session}/_set.py +8 -8
- cognite/neat/{session → _v0/session}/_show.py +5 -5
- cognite/neat/{session → _v0/session}/_state.py +10 -10
- cognite/neat/{session → _v0/session}/_subset.py +4 -4
- cognite/neat/{session → _v0/session}/_template.py +11 -11
- cognite/neat/{session → _v0/session}/_to.py +12 -12
- cognite/neat/{session → _v0/session}/_wizard.py +1 -1
- cognite/neat/{session → _v0/session}/exceptions.py +5 -5
- cognite/neat/_version.py +1 -1
- cognite/neat/legacy.py +6 -0
- cognite_neat-1.0.22.dist-info/METADATA +123 -0
- cognite_neat-1.0.22.dist-info/RECORD +329 -0
- cognite_neat-1.0.22.dist-info/WHEEL +4 -0
- cognite/neat/core/_data_model/importers/_rdf/_owl2data_model.py +0 -91
- cognite/neat/core/_data_model/models/entities/_loaders.py +0 -75
- cognite/neat/plugins/__init__.py +0 -3
- cognite/neat/plugins/data_model/importers/__init__.py +0 -5
- cognite/neat/plugins/data_model/importers/_base.py +0 -28
- cognite/neat/session/_session/_data_model/__init__.py +0 -3
- cognite/neat/session/_session/_data_model/_read.py +0 -193
- cognite/neat/session/_session/_data_model/_routes.py +0 -45
- cognite/neat/session/_session/_data_model/_show.py +0 -147
- cognite/neat/session/_session/_data_model/_write.py +0 -335
- cognite_neat-0.123.24.dist-info/METADATA +0 -144
- cognite_neat-0.123.24.dist-info/RECORD +0 -201
- cognite_neat-0.123.24.dist-info/WHEEL +0 -4
- cognite_neat-0.123.24.dist-info/licenses/LICENSE +0 -201
- /cognite/neat/{core → _client/init}/__init__.py +0 -0
- /cognite/neat/{core/_client/_api → _data_model}/__init__.py +0 -0
- /cognite/neat/{core/_client/data_classes → _data_model/deployer}/__init__.py +0 -0
- /cognite/neat/{core/_data_model → _data_model/exporters/_table_exporter}/__init__.py +0 -0
- /cognite/neat/{core/_instances → _data_model/importers/_table_importer}/__init__.py +0 -0
- /cognite/neat/{core/_instances/extractors/_classic_cdf → _data_model/models}/__init__.py +0 -0
- /cognite/neat/{core/_utils → _data_model/models/conceptual}/__init__.py +0 -0
- /cognite/neat/{plugins/data_model → _data_model/validation}/__init__.py +0 -0
- /cognite/neat/{session/_session → _session/_html}/__init__.py +0 -0
- /cognite/neat/{core → _v0/core}/_client/__init__.py +0 -0
- /cognite/neat/{core → _v0/core}/_client/data_classes/data_modeling.py +0 -0
- /cognite/neat/{core → _v0/core}/_client/data_classes/neat_sequence.py +0 -0
- /cognite/neat/{core → _v0/core}/_client/data_classes/statistics.py +0 -0
- /cognite/neat/{core → _v0/core}/_config.py +0 -0
- /cognite/neat/{core → _v0/core}/_data_model/analysis/__init__.py +0 -0
- /cognite/neat/{core → _v0/core}/_data_model/catalog/__init__.py +0 -0
- /cognite/neat/{core → _v0/core}/_data_model/catalog/classic_model.xlsx +0 -0
- /cognite/neat/{core → _v0/core}/_data_model/catalog/conceptual-imf-data-model.xlsx +0 -0
- /cognite/neat/{core → _v0/core}/_data_model/catalog/hello_world_pump.xlsx +0 -0
- /cognite/neat/{core → _v0/core}/_data_model/importers/__init__.py +0 -0
- /cognite/neat/{core → _v0/core}/_data_model/importers/_rdf/__init__.py +0 -0
- /cognite/neat/{core → _v0/core}/_data_model/models/_base_unverified.py +0 -0
- /cognite/neat/{core → _v0/core}/_data_model/models/conceptual/__init__.py +0 -0
- /cognite/neat/{core → _v0/core}/_data_model/models/entities/_constants.py +0 -0
- /cognite/neat/{core → _v0/core}/_data_model/models/entities/_wrapped.py +0 -0
- /cognite/neat/{core → _v0/core}/_data_model/models/mapping/__init__.py +0 -0
- /cognite/neat/{core → _v0/core}/_data_model/models/mapping/_classic2core.yaml +0 -0
- /cognite/neat/{core → _v0/core}/_data_model/transformers/__init__.py +0 -0
- /cognite/neat/{core → _v0/core}/_instances/_shared.py +0 -0
- /cognite/neat/{core → _v0/core}/_instances/_tracking/__init__.py +0 -0
- /cognite/neat/{core → _v0/core}/_instances/examples/Knowledge-Graph-Nordic44-dirty.xml +0 -0
- /cognite/neat/{core → _v0/core}/_instances/examples/Knowledge-Graph-Nordic44.xml +0 -0
- /cognite/neat/{core → _v0/core}/_instances/examples/__init__.py +0 -0
- /cognite/neat/{core → _v0/core}/_instances/examples/skos-capturing-sheet-wind-topics.xlsx +0 -0
- /cognite/neat/{core → _v0/core}/_instances/extractors/_classic_cdf/_assets.py +0 -0
- /cognite/neat/{core → _v0/core}/_instances/extractors/_classic_cdf/_data_sets.py +0 -0
- /cognite/neat/{core → _v0/core}/_instances/extractors/_classic_cdf/_events.py +0 -0
- /cognite/neat/{core → _v0/core}/_instances/extractors/_classic_cdf/_files.py +0 -0
- /cognite/neat/{core → _v0/core}/_instances/extractors/_classic_cdf/_labels.py +0 -0
- /cognite/neat/{core → _v0/core}/_instances/extractors/_classic_cdf/_timeseries.py +0 -0
- /cognite/neat/{core → _v0/core}/_instances/loaders/__init__.py +0 -0
- /cognite/neat/{core → _v0/core}/_instances/queries/__init__.py +0 -0
- /cognite/neat/{core → _v0/core}/_instances/queries/_base.py +0 -0
- /cognite/neat/{core → _v0/core}/_instances/queries/_queries.py +0 -0
- /cognite/neat/{core → _v0/core}/_instances/transformers/__init__.py +0 -0
- /cognite/neat/{core → _v0/core}/_issues/__init__.py +0 -0
- /cognite/neat/{core → _v0/core}/_issues/formatters.py +0 -0
- /cognite/neat/{core → _v0/core}/_shared.py +0 -0
- /cognite/neat/{core → _v0/core}/_store/__init__.py +0 -0
- /cognite/neat/{core → _v0/core}/_utils/io_.py +0 -0
- /cognite/neat/{core → _v0/core}/_utils/reader/__init__.py +0 -0
- /cognite/neat/{core → _v0/core}/_utils/tarjan.py +0 -0
- /cognite/neat/{core → _v0/core}/_utils/time_.py +0 -0
- /cognite/neat/{core → _v0/core}/_utils/xml_.py +0 -0
- /cognite/neat/{session → _v0}/engine/__init__.py +0 -0
- /cognite/neat/{session → _v0}/engine/_import.py +0 -0
- /cognite/neat/{session → _v0}/engine/_interface.py +0 -0
- /cognite/neat/{session → _v0/session}/__init__.py +0 -0
- /cognite/neat/{session → _v0/session}/_experimental.py +0 -0
- /cognite/neat/{session → _v0/session}/_state/README.md +0 -0
|
@@ -0,0 +1,644 @@
|
|
|
1
|
+
import itertools
|
|
2
|
+
import sys
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
from collections import UserList, defaultdict
|
|
5
|
+
from collections.abc import Hashable, Sequence
|
|
6
|
+
from enum import Enum
|
|
7
|
+
from typing import Any, Generic, Literal, TypeAlias, cast
|
|
8
|
+
|
|
9
|
+
from pydantic import BaseModel, Field
|
|
10
|
+
from pydantic.alias_generators import to_camel
|
|
11
|
+
|
|
12
|
+
from cognite.neat._data_model._snapshot import SchemaSnapshot
|
|
13
|
+
from cognite.neat._data_model.models.dms import (
|
|
14
|
+
BaseModelObject,
|
|
15
|
+
Constraint,
|
|
16
|
+
ContainerConstraintReference,
|
|
17
|
+
ContainerIndexReference,
|
|
18
|
+
ContainerPropertyDefinition,
|
|
19
|
+
ContainerReference,
|
|
20
|
+
ContainerRequest,
|
|
21
|
+
DataModelRequest,
|
|
22
|
+
DataModelResource,
|
|
23
|
+
Index,
|
|
24
|
+
T_DataModelResource,
|
|
25
|
+
T_ResourceId,
|
|
26
|
+
ViewRequest,
|
|
27
|
+
ViewRequestProperty,
|
|
28
|
+
)
|
|
29
|
+
from cognite.neat._utils.http_client import (
|
|
30
|
+
FailedRequestItems,
|
|
31
|
+
FailedResponseItems,
|
|
32
|
+
SuccessResponse,
|
|
33
|
+
SuccessResponseItems,
|
|
34
|
+
)
|
|
35
|
+
from cognite.neat._utils.useful_types import T_Reference
|
|
36
|
+
|
|
37
|
+
if sys.version_info >= (3, 11):
|
|
38
|
+
from typing import Self
|
|
39
|
+
else:
|
|
40
|
+
from typing_extensions import Self
|
|
41
|
+
|
|
42
|
+
JsonPath: TypeAlias = str # e.g., 'properties.temperature', 'constraints.uniqueKey'
|
|
43
|
+
DataModelEndpoint: TypeAlias = Literal["spaces", "containers", "views", "datamodels", "instances"]
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class SeverityType(Enum):
|
|
47
|
+
SAFE = 1
|
|
48
|
+
WARNING = 2
|
|
49
|
+
BREAKING = 3
|
|
50
|
+
|
|
51
|
+
@classmethod
|
|
52
|
+
def max_severity(cls, severities: list["SeverityType"], default: "SeverityType") -> "SeverityType":
|
|
53
|
+
value = max([severity.value for severity in severities], default=default.value)
|
|
54
|
+
return cls(value)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class BaseDeployObject(BaseModel, alias_generator=to_camel, extra="ignore", populate_by_name=True):
|
|
58
|
+
"""Base class for all deployer data model objects."""
|
|
59
|
+
|
|
60
|
+
...
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class FieldChange(BaseDeployObject, ABC):
|
|
64
|
+
"""Represents a change to a specific property or field."""
|
|
65
|
+
|
|
66
|
+
field_path: JsonPath
|
|
67
|
+
|
|
68
|
+
@property
|
|
69
|
+
@abstractmethod
|
|
70
|
+
def severity(self) -> SeverityType:
|
|
71
|
+
"""The severity of the change."""
|
|
72
|
+
raise NotImplementedError()
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class PrimitiveField(FieldChange, ABC):
|
|
76
|
+
"""Base class for changes to primitive properties."""
|
|
77
|
+
|
|
78
|
+
item_severity: SeverityType
|
|
79
|
+
|
|
80
|
+
@property
|
|
81
|
+
def severity(self) -> SeverityType:
|
|
82
|
+
return self.item_severity
|
|
83
|
+
|
|
84
|
+
@property
|
|
85
|
+
@abstractmethod
|
|
86
|
+
def description(self) -> str:
|
|
87
|
+
"""Human-readable description of the change."""
|
|
88
|
+
...
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class AddedField(PrimitiveField):
|
|
92
|
+
new_value: BaseModelObject | str | int | float | bool | None
|
|
93
|
+
|
|
94
|
+
@property
|
|
95
|
+
def description(self) -> str:
|
|
96
|
+
return f"added with value {self.new_value!r}"
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class RemovedField(PrimitiveField):
|
|
100
|
+
current_value: BaseModelObject | str | int | float | bool | None
|
|
101
|
+
|
|
102
|
+
@property
|
|
103
|
+
def description(self) -> str:
|
|
104
|
+
return f"removed (was {self.current_value!r})"
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class ChangedField(PrimitiveField):
|
|
108
|
+
new_value: BaseModelObject | str | int | float | bool | None
|
|
109
|
+
current_value: BaseModelObject | str | int | float | bool | None
|
|
110
|
+
|
|
111
|
+
@property
|
|
112
|
+
def description(self) -> str:
|
|
113
|
+
if self.new_value is None:
|
|
114
|
+
return f"removed (was {self.current_value!r})"
|
|
115
|
+
elif self.current_value is None:
|
|
116
|
+
return f"added with value {self.new_value!r}"
|
|
117
|
+
return f"changed from {self.current_value!r} to {self.new_value!r}"
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class FieldChanges(FieldChange):
|
|
121
|
+
"""Represents a nested property, i.e., a property that contains other properties."""
|
|
122
|
+
|
|
123
|
+
changes: list[FieldChange]
|
|
124
|
+
|
|
125
|
+
@property
|
|
126
|
+
def severity(self) -> SeverityType:
|
|
127
|
+
return SeverityType.max_severity([item.severity for item in self.changes], default=SeverityType.SAFE)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
class ResourceChange(BaseDeployObject, Generic[T_ResourceId, T_DataModelResource]):
|
|
131
|
+
resource_id: T_ResourceId
|
|
132
|
+
new_value: T_DataModelResource | None
|
|
133
|
+
current_value: T_DataModelResource | None = None
|
|
134
|
+
changes: list[FieldChange] = Field(default_factory=list)
|
|
135
|
+
message: str | None = None
|
|
136
|
+
|
|
137
|
+
@property
|
|
138
|
+
def change_type(self) -> Literal["create", "update", "delete", "unchanged", "skip"]:
|
|
139
|
+
if self.current_value is None and self.new_value is not None:
|
|
140
|
+
return "create"
|
|
141
|
+
elif self.new_value is None and self.current_value is not None:
|
|
142
|
+
return "delete"
|
|
143
|
+
elif self.changes:
|
|
144
|
+
return "update"
|
|
145
|
+
elif self.new_value is None and self.current_value is None:
|
|
146
|
+
return "skip"
|
|
147
|
+
else:
|
|
148
|
+
return "unchanged"
|
|
149
|
+
|
|
150
|
+
@property
|
|
151
|
+
def severity(self) -> SeverityType:
|
|
152
|
+
return SeverityType.max_severity([change.severity for change in self.changes], default=SeverityType.SAFE)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
class ResourceDeploymentPlan(BaseDeployObject, Generic[T_ResourceId, T_DataModelResource]):
|
|
156
|
+
endpoint: DataModelEndpoint
|
|
157
|
+
resources: list[ResourceChange[T_ResourceId, T_DataModelResource]]
|
|
158
|
+
|
|
159
|
+
@property
|
|
160
|
+
def to_upsert(self) -> list[ResourceChange[T_ResourceId, T_DataModelResource]]:
|
|
161
|
+
return [change for change in self.resources if change.change_type in ("create", "update")]
|
|
162
|
+
|
|
163
|
+
@property
|
|
164
|
+
def to_create(self) -> list[ResourceChange[T_ResourceId, T_DataModelResource]]:
|
|
165
|
+
return [change for change in self.resources if change.change_type == "create"]
|
|
166
|
+
|
|
167
|
+
@property
|
|
168
|
+
def to_update(self) -> list[ResourceChange[T_ResourceId, T_DataModelResource]]:
|
|
169
|
+
return [change for change in self.resources if change.change_type == "update"]
|
|
170
|
+
|
|
171
|
+
@property
|
|
172
|
+
def to_delete(self) -> list[ResourceChange[T_ResourceId, T_DataModelResource]]:
|
|
173
|
+
return [change for change in self.resources if change.change_type == "delete"]
|
|
174
|
+
|
|
175
|
+
@property
|
|
176
|
+
def unchanged(self) -> list[ResourceChange[T_ResourceId, T_DataModelResource]]:
|
|
177
|
+
return [change for change in self.resources if change.change_type == "unchanged"]
|
|
178
|
+
|
|
179
|
+
@property
|
|
180
|
+
def skip(self) -> list[ResourceChange[T_ResourceId, T_DataModelResource]]:
|
|
181
|
+
return [change for change in self.resources if change.change_type == "skip"]
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
class ContainerDeploymentPlan(ResourceDeploymentPlan[ContainerReference, ContainerRequest]):
|
|
185
|
+
endpoint: Literal["containers"] = "containers"
|
|
186
|
+
resources: list[ResourceChange[ContainerReference, ContainerRequest]]
|
|
187
|
+
|
|
188
|
+
@property
|
|
189
|
+
def indexes_to_remove(self) -> dict[ContainerIndexReference, RemovedField]:
|
|
190
|
+
return self._get_fields_to_remove("indexes.", ContainerIndexReference)
|
|
191
|
+
|
|
192
|
+
@property
|
|
193
|
+
def constraints_to_remove(self) -> dict[ContainerConstraintReference, RemovedField]:
|
|
194
|
+
return self._get_fields_to_remove("constraints.", ContainerConstraintReference)
|
|
195
|
+
|
|
196
|
+
def _get_fields_to_remove(self, field_prefix: str, ref_cls: type) -> dict:
|
|
197
|
+
items: dict = {}
|
|
198
|
+
for resource_change in self.resources:
|
|
199
|
+
for change in resource_change.changes:
|
|
200
|
+
if isinstance(change, RemovedField) and change.field_path.startswith(field_prefix):
|
|
201
|
+
identifier = change.field_path.removeprefix(field_prefix)
|
|
202
|
+
items[
|
|
203
|
+
ref_cls(
|
|
204
|
+
space=resource_change.resource_id.space,
|
|
205
|
+
external_id=resource_change.resource_id.external_id,
|
|
206
|
+
identifier=identifier,
|
|
207
|
+
)
|
|
208
|
+
] = change
|
|
209
|
+
return items
|
|
210
|
+
|
|
211
|
+
@property
|
|
212
|
+
def to_upsert(self) -> list[ResourceChange[ContainerReference, ContainerRequest]]:
|
|
213
|
+
return [change for change in self.resources if change.change_type == "create" or self._is_update(change)]
|
|
214
|
+
|
|
215
|
+
@property
|
|
216
|
+
def to_update(self) -> list[ResourceChange[ContainerReference, ContainerRequest]]:
|
|
217
|
+
return [change for change in self.resources if self._is_update(change)]
|
|
218
|
+
|
|
219
|
+
@classmethod
|
|
220
|
+
def _is_update(cls, change: ResourceChange[ContainerReference, ContainerRequest]) -> bool:
|
|
221
|
+
"""Whether the container change is an update.
|
|
222
|
+
|
|
223
|
+
Containers with only index or constraint removals are not considered updates, as these are handled by a
|
|
224
|
+
separate API call.
|
|
225
|
+
"""
|
|
226
|
+
if change.change_type != "update":
|
|
227
|
+
return False
|
|
228
|
+
for c in change.changes:
|
|
229
|
+
if not (
|
|
230
|
+
isinstance(c, RemovedField)
|
|
231
|
+
and (c.field_path.startswith("indexes.") or c.field_path.startswith("constraints."))
|
|
232
|
+
):
|
|
233
|
+
return True
|
|
234
|
+
return False
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
class ResourceDeploymentPlanList(UserList[ResourceDeploymentPlan]):
|
|
238
|
+
def consolidate_changes(self) -> Self:
|
|
239
|
+
"""Consolidate the deployment plans by applying field removals to the new_value of resources."""
|
|
240
|
+
return type(self)([self._consolidate_resource_plan(plan) for plan in self.data])
|
|
241
|
+
|
|
242
|
+
def _consolidate_resource_plan(self, plan: ResourceDeploymentPlan) -> ResourceDeploymentPlan:
|
|
243
|
+
consolidated_resources = [
|
|
244
|
+
self._consolidate_resource_change(resource_change) for resource_change in plan.resources
|
|
245
|
+
]
|
|
246
|
+
return plan.model_copy(update={"resources": consolidated_resources})
|
|
247
|
+
|
|
248
|
+
def _consolidate_resource_change(
|
|
249
|
+
self, resource: ResourceChange[T_ResourceId, T_DataModelResource]
|
|
250
|
+
) -> ResourceChange[T_ResourceId, T_DataModelResource]:
|
|
251
|
+
if resource.new_value is None and resource.current_value is not None:
|
|
252
|
+
# Changed deletion (new_value is None and curren_value is not None) to unchanged by copying
|
|
253
|
+
# current_value to new_value.
|
|
254
|
+
updated_resource = resource.model_copy(update={"new_value": resource.current_value})
|
|
255
|
+
elif resource.changes and resource.new_value is not None:
|
|
256
|
+
# Find all field removals and update new_value accordingly.
|
|
257
|
+
removals = [change for change in resource.changes if isinstance(change, RemovedField)]
|
|
258
|
+
addition_paths = {change.field_path for change in resource.changes if isinstance(change, AddedField)}
|
|
259
|
+
if removals:
|
|
260
|
+
if resource.current_value is None:
|
|
261
|
+
raise RuntimeError("Bug in Neat: current_value is None for a resource with removals.")
|
|
262
|
+
new_value = self._consolidate_resource(
|
|
263
|
+
resource.current_value, resource.new_value, removals, addition_paths
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
updated_resource = resource.model_copy(
|
|
267
|
+
update={
|
|
268
|
+
"new_value": new_value,
|
|
269
|
+
"changes": [
|
|
270
|
+
change
|
|
271
|
+
for change in resource.changes
|
|
272
|
+
if not isinstance(change, RemovedField)
|
|
273
|
+
or (isinstance(change, RemovedField) and change.field_path in addition_paths)
|
|
274
|
+
],
|
|
275
|
+
}
|
|
276
|
+
)
|
|
277
|
+
else:
|
|
278
|
+
# No removals, keep as is.
|
|
279
|
+
updated_resource = resource
|
|
280
|
+
else:
|
|
281
|
+
# Creation or unchanged, keep as is.
|
|
282
|
+
updated_resource = resource
|
|
283
|
+
return updated_resource
|
|
284
|
+
|
|
285
|
+
def _consolidate_resource(
|
|
286
|
+
self, current: DataModelResource, new: DataModelResource, removals: list[RemovedField], addition_paths: set[str]
|
|
287
|
+
) -> DataModelResource:
|
|
288
|
+
if isinstance(new, DataModelRequest):
|
|
289
|
+
if not isinstance(current, DataModelRequest):
|
|
290
|
+
# This should not happen, as only containers, views, and data models have removable fields.
|
|
291
|
+
raise RuntimeError("Bug in Neat: current value is not a DataModelRequest during consolidation.")
|
|
292
|
+
return self._consolidate_data_model(current, new)
|
|
293
|
+
elif isinstance(new, ViewRequest):
|
|
294
|
+
return self._consolidate_view(new, removals)
|
|
295
|
+
elif isinstance(new, ContainerRequest):
|
|
296
|
+
return self._consolidate_container(new, removals, addition_paths)
|
|
297
|
+
elif removals:
|
|
298
|
+
# This should not happen, as only containers, views, and data models have removable fields.
|
|
299
|
+
raise RuntimeError("Bug in Neat: attempted to consolidate removals for unsupported resource type.")
|
|
300
|
+
return new
|
|
301
|
+
|
|
302
|
+
@staticmethod
|
|
303
|
+
def _consolidate_data_model(current: DataModelRequest, new: DataModelRequest) -> DataModelResource:
|
|
304
|
+
current_views = set(v for v in (current.views or []))
|
|
305
|
+
new_only_views = [v for v in (new.views or []) if v not in current_views]
|
|
306
|
+
final_views = (current.views or []) + new_only_views
|
|
307
|
+
return new.model_copy(update={"views": final_views}, deep=True)
|
|
308
|
+
|
|
309
|
+
@staticmethod
|
|
310
|
+
def _consolidate_view(resource: ViewRequest, removals: list[RemovedField]) -> DataModelResource:
|
|
311
|
+
view_properties = resource.properties.copy()
|
|
312
|
+
for removal in removals:
|
|
313
|
+
if removal.field_path.startswith("properties."):
|
|
314
|
+
prop_key = removal.field_path.removeprefix("properties.")
|
|
315
|
+
view_properties[prop_key] = cast(ViewRequestProperty, removal.current_value)
|
|
316
|
+
return resource.model_copy(update={"properties": view_properties}, deep=True)
|
|
317
|
+
|
|
318
|
+
@staticmethod
|
|
319
|
+
def _consolidate_container(
|
|
320
|
+
resource: ContainerRequest, removals: list[RemovedField], addition_paths: set[str]
|
|
321
|
+
) -> DataModelResource:
|
|
322
|
+
container_properties = resource.properties.copy()
|
|
323
|
+
indexes = (resource.indexes or {}).copy()
|
|
324
|
+
constraints = (resource.constraints or {}).copy()
|
|
325
|
+
for removal in removals:
|
|
326
|
+
if removal.field_path.startswith("properties."):
|
|
327
|
+
prop_key = removal.field_path.removeprefix("properties.")
|
|
328
|
+
container_properties[prop_key] = cast(ContainerPropertyDefinition, removal.current_value)
|
|
329
|
+
elif removal.field_path.startswith("indexes.") and removal.field_path not in addition_paths:
|
|
330
|
+
# Index was removed and not re-added, so we need to restore it.
|
|
331
|
+
index_key = removal.field_path.removeprefix("indexes.")
|
|
332
|
+
indexes[index_key] = cast(Index, removal.current_value)
|
|
333
|
+
elif removal.field_path.startswith("constraints.") and removal.field_path not in addition_paths:
|
|
334
|
+
# Constraint was removed and not re-added, so we need to restore it.
|
|
335
|
+
constraint_key = removal.field_path.removeprefix("constraints.")
|
|
336
|
+
constraints[constraint_key] = cast(Constraint, removal.current_value)
|
|
337
|
+
return resource.model_copy(
|
|
338
|
+
update={"properties": container_properties, "indexes": indexes or None, "constraints": constraints or None},
|
|
339
|
+
deep=True,
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
def force_changes(self, drop_data: bool) -> Self:
|
|
343
|
+
"""Force all resources by deleting and recreating them.
|
|
344
|
+
|
|
345
|
+
Args:
|
|
346
|
+
drop_data: If True, containers will be deleted and recreated. If False, containers
|
|
347
|
+
will be consolidated instead.
|
|
348
|
+
Returns:
|
|
349
|
+
A new ResourceDeploymentPlanList with forced changes.
|
|
350
|
+
"""
|
|
351
|
+
forced_plans: list[ResourceDeploymentPlan] = []
|
|
352
|
+
for plan in self.data:
|
|
353
|
+
forced_resources: list[ResourceChange] = []
|
|
354
|
+
for resource in plan.resources:
|
|
355
|
+
if resource.change_type == "update" and resource.severity == SeverityType.BREAKING:
|
|
356
|
+
if drop_data or plan.endpoint != "containers":
|
|
357
|
+
deletion = resource.model_copy(deep=True, update={"new_value": None, "changes": []})
|
|
358
|
+
recreation = resource.model_copy(deep=True, update={"current_value": None, "changes": []})
|
|
359
|
+
forced_resources.append(deletion)
|
|
360
|
+
forced_resources.append(recreation)
|
|
361
|
+
else:
|
|
362
|
+
# For containers, we try to consolidate instead of deleting and recreating.
|
|
363
|
+
# Note that there might still be breaking changes left which will cause the deployment to fail.
|
|
364
|
+
# For example, if the usedFor field has changed from node->edge, then this cannot be
|
|
365
|
+
# consolidated.
|
|
366
|
+
consolidated_resource = self._consolidate_resource_change(resource)
|
|
367
|
+
forced_resources.append(consolidated_resource)
|
|
368
|
+
else:
|
|
369
|
+
# No need to force, keep as is.
|
|
370
|
+
forced_resources.append(resource)
|
|
371
|
+
forced_plans.append(plan.model_copy(update={"resources": forced_resources}))
|
|
372
|
+
return type(self)(forced_plans)
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
class ChangeResult(BaseDeployObject, Generic[T_ResourceId, T_DataModelResource], ABC):
|
|
376
|
+
endpoint: DataModelEndpoint
|
|
377
|
+
change: ResourceChange[T_ResourceId, T_DataModelResource]
|
|
378
|
+
|
|
379
|
+
@property
|
|
380
|
+
@abstractmethod
|
|
381
|
+
def message(self) -> str:
|
|
382
|
+
"""Human-readable message about the change result."""
|
|
383
|
+
...
|
|
384
|
+
|
|
385
|
+
@property
|
|
386
|
+
@abstractmethod
|
|
387
|
+
def is_success(self) -> bool:
|
|
388
|
+
"""Whether the change was successful."""
|
|
389
|
+
...
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
class HTTPChangeResult(ChangeResult[T_ResourceId, T_DataModelResource]):
|
|
393
|
+
http_message: (
|
|
394
|
+
SuccessResponseItems[T_ResourceId] | FailedResponseItems[T_ResourceId] | FailedRequestItems[T_ResourceId]
|
|
395
|
+
)
|
|
396
|
+
|
|
397
|
+
@property
|
|
398
|
+
def message(self) -> str:
|
|
399
|
+
if isinstance(self.http_message, SuccessResponse):
|
|
400
|
+
return "Success"
|
|
401
|
+
elif isinstance(self.http_message, FailedResponseItems):
|
|
402
|
+
error = self.http_message.error
|
|
403
|
+
return f"Failed: {error.code} | {error.message}"
|
|
404
|
+
elif isinstance(self.http_message, FailedRequestItems):
|
|
405
|
+
return f"Request Failed: {self.http_message.message}"
|
|
406
|
+
else:
|
|
407
|
+
return "Unknown result"
|
|
408
|
+
|
|
409
|
+
@property
|
|
410
|
+
def is_success(self) -> bool:
|
|
411
|
+
return isinstance(self.http_message, SuccessResponse)
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
class MultiHTTPChangeResult(ChangeResult[T_ResourceId, T_DataModelResource]):
|
|
415
|
+
http_messages: list[
|
|
416
|
+
SuccessResponseItems[T_ResourceId] | FailedResponseItems[T_ResourceId] | FailedRequestItems[T_ResourceId]
|
|
417
|
+
]
|
|
418
|
+
|
|
419
|
+
@property
|
|
420
|
+
def message(self) -> str:
|
|
421
|
+
error_messages: list[str] = []
|
|
422
|
+
for msg in self.http_messages:
|
|
423
|
+
if isinstance(msg, SuccessResponse):
|
|
424
|
+
continue
|
|
425
|
+
elif isinstance(msg, FailedResponseItems):
|
|
426
|
+
error = msg.error
|
|
427
|
+
error_messages.append(f"Failed: {error.code} | {error.message}")
|
|
428
|
+
elif isinstance(msg, FailedRequestItems):
|
|
429
|
+
error_messages.append(f"Request Failed: {msg.message}")
|
|
430
|
+
if not error_messages:
|
|
431
|
+
return "Success"
|
|
432
|
+
return "; ".join(error_messages)
|
|
433
|
+
|
|
434
|
+
@property
|
|
435
|
+
def is_success(self) -> bool:
|
|
436
|
+
return all(isinstance(msg, SuccessResponse) for msg in self.http_messages)
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
class NoOpChangeResult(ChangeResult[T_ResourceId, T_DataModelResource]):
|
|
440
|
+
"""A change result representing a no-op, e.g., when a change was skipped or unchanged."""
|
|
441
|
+
|
|
442
|
+
reason: str
|
|
443
|
+
|
|
444
|
+
@property
|
|
445
|
+
def message(self) -> str:
|
|
446
|
+
return self.reason
|
|
447
|
+
|
|
448
|
+
@property
|
|
449
|
+
def is_success(self) -> bool:
|
|
450
|
+
return True
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
class ChangedFieldResult(BaseDeployObject, Generic[T_ResourceId, T_Reference]):
|
|
454
|
+
resource_id: T_ResourceId
|
|
455
|
+
field_change: FieldChange
|
|
456
|
+
http_message: SuccessResponseItems[T_Reference] | FailedResponseItems[T_Reference] | FailedRequestItems[T_Reference]
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
class AppliedChanges(BaseDeployObject):
|
|
460
|
+
"""The result of applying changes to the data model.
|
|
461
|
+
|
|
462
|
+
Contains lists of created, updated, deleted, and unchanged resources.
|
|
463
|
+
|
|
464
|
+
In addition, it has changed fields which tracks the removal of indexes and constraints from containers.
|
|
465
|
+
This is needed as these changes are done with a separate API call per change.
|
|
466
|
+
"""
|
|
467
|
+
|
|
468
|
+
created: list[HTTPChangeResult] = Field(default_factory=list)
|
|
469
|
+
updated: list[HTTPChangeResult] = Field(default_factory=list)
|
|
470
|
+
deletions: list[HTTPChangeResult] = Field(default_factory=list)
|
|
471
|
+
unchanged: list[NoOpChangeResult] = Field(default_factory=list)
|
|
472
|
+
skipped: list[NoOpChangeResult] = Field(default_factory=list)
|
|
473
|
+
changed_fields: list[ChangedFieldResult] = Field(default_factory=list)
|
|
474
|
+
|
|
475
|
+
@property
|
|
476
|
+
def is_success(self) -> bool:
|
|
477
|
+
return all(
|
|
478
|
+
# MyPy fails to understand that ChangeFieldResult.message has the same structure as ChangeResult.message
|
|
479
|
+
isinstance(change.http_message, SuccessResponse) # type: ignore[attr-defined]
|
|
480
|
+
for change in itertools.chain(self.created, self.updated, self.deletions, self.changed_fields)
|
|
481
|
+
)
|
|
482
|
+
|
|
483
|
+
@property
|
|
484
|
+
def merged_updated(self) -> Sequence[ChangeResult]:
|
|
485
|
+
"""Merges the changed field into the updated changes."""
|
|
486
|
+
if not self.changed_fields:
|
|
487
|
+
return self.updated
|
|
488
|
+
changed_fields_by_id: dict[Hashable, list[ChangedFieldResult]] = defaultdict(list)
|
|
489
|
+
for changed_field in self.changed_fields:
|
|
490
|
+
changed_fields_by_id[changed_field.resource_id].append(changed_field)
|
|
491
|
+
merged_changes: list[ChangeResult] = []
|
|
492
|
+
for update in self.updated:
|
|
493
|
+
if update.change.resource_id not in changed_fields_by_id:
|
|
494
|
+
merged_changes.append(update)
|
|
495
|
+
continue
|
|
496
|
+
|
|
497
|
+
field_changes = changed_fields_by_id[update.change.resource_id]
|
|
498
|
+
merged_change = update.change.model_copy(
|
|
499
|
+
update={"changes": update.change.changes + [fc.field_change for fc in field_changes]}
|
|
500
|
+
)
|
|
501
|
+
|
|
502
|
+
# MyPy wants an annotation were we want this to be generic.
|
|
503
|
+
merged_result = MultiHTTPChangeResult( # type: ignore[var-annotated]
|
|
504
|
+
endpoint=update.endpoint,
|
|
505
|
+
change=merged_change,
|
|
506
|
+
http_messages=[update.http_message] + [fc.http_message for fc in field_changes],
|
|
507
|
+
)
|
|
508
|
+
merged_changes.append(merged_result)
|
|
509
|
+
|
|
510
|
+
return merged_changes
|
|
511
|
+
|
|
512
|
+
def as_recovery_plan(self) -> list[ResourceDeploymentPlan]:
|
|
513
|
+
"""Generate a recovery plan based on the applied changes."""
|
|
514
|
+
recovery_plan: dict[DataModelEndpoint, ResourceDeploymentPlan] = {}
|
|
515
|
+
for change_result in itertools.chain(self.created, self.updated, self.deletions):
|
|
516
|
+
if not isinstance(change_result.http_message, SuccessResponse):
|
|
517
|
+
continue # Skip failed changes.
|
|
518
|
+
change = change_result.change
|
|
519
|
+
if change.change_type == "create":
|
|
520
|
+
# To recover a created resource, we need to delete it.
|
|
521
|
+
# MyPy wants an annotation were we want this to be generic.
|
|
522
|
+
recovery_change = ResourceChange( # type: ignore[var-annotated]
|
|
523
|
+
resource_id=change.resource_id,
|
|
524
|
+
current_value=change.new_value,
|
|
525
|
+
new_value=None,
|
|
526
|
+
changes=[],
|
|
527
|
+
)
|
|
528
|
+
elif change.change_type == "delete":
|
|
529
|
+
# To recover a deleted resource, we need to create it.
|
|
530
|
+
recovery_change = ResourceChange(
|
|
531
|
+
resource_id=change.resource_id,
|
|
532
|
+
current_value=None,
|
|
533
|
+
new_value=change.current_value,
|
|
534
|
+
changes=[],
|
|
535
|
+
)
|
|
536
|
+
elif change.change_type == "update":
|
|
537
|
+
# To recover an updated resource, we need to revert to the previous state.
|
|
538
|
+
recovery_change = ResourceChange(
|
|
539
|
+
resource_id=change.resource_id,
|
|
540
|
+
current_value=change.new_value,
|
|
541
|
+
new_value=change.current_value,
|
|
542
|
+
changes=self._reverse_changes(change.changes),
|
|
543
|
+
)
|
|
544
|
+
else:
|
|
545
|
+
continue # Unchanged resources do not need recovery.
|
|
546
|
+
|
|
547
|
+
if change_result.endpoint not in recovery_plan:
|
|
548
|
+
recovery_plan[change_result.endpoint] = ResourceDeploymentPlan(
|
|
549
|
+
endpoint=change_result.endpoint, resources=[]
|
|
550
|
+
)
|
|
551
|
+
recovery_plan[change_result.endpoint].resources.append(recovery_change)
|
|
552
|
+
|
|
553
|
+
return list(recovery_plan.values())
|
|
554
|
+
|
|
555
|
+
def _reverse_changes(self, changes: list[FieldChange]) -> list[FieldChange]:
|
|
556
|
+
reversed_changes: list[FieldChange] = []
|
|
557
|
+
for change in changes:
|
|
558
|
+
if isinstance(change, AddedField):
|
|
559
|
+
reversed_changes.append(
|
|
560
|
+
RemovedField(
|
|
561
|
+
field_path=change.field_path,
|
|
562
|
+
current_value=change.new_value,
|
|
563
|
+
item_severity=change.item_severity,
|
|
564
|
+
)
|
|
565
|
+
)
|
|
566
|
+
elif isinstance(change, RemovedField):
|
|
567
|
+
reversed_changes.append(
|
|
568
|
+
AddedField(
|
|
569
|
+
field_path=change.field_path,
|
|
570
|
+
new_value=change.current_value,
|
|
571
|
+
item_severity=change.item_severity,
|
|
572
|
+
)
|
|
573
|
+
)
|
|
574
|
+
elif isinstance(change, ChangedField):
|
|
575
|
+
reversed_changes.append(
|
|
576
|
+
ChangedField(
|
|
577
|
+
field_path=change.field_path,
|
|
578
|
+
current_value=change.new_value,
|
|
579
|
+
new_value=change.current_value,
|
|
580
|
+
item_severity=change.item_severity,
|
|
581
|
+
)
|
|
582
|
+
)
|
|
583
|
+
elif isinstance(change, FieldChanges):
|
|
584
|
+
reversed_changes.append(
|
|
585
|
+
FieldChanges(
|
|
586
|
+
field_path=change.field_path,
|
|
587
|
+
changes=self._reverse_changes(change.changes),
|
|
588
|
+
)
|
|
589
|
+
)
|
|
590
|
+
return reversed_changes
|
|
591
|
+
|
|
592
|
+
|
|
593
|
+
class DeploymentResult(BaseDeployObject):
|
|
594
|
+
status: Literal["success", "failure", "partial", "pending", "recovered", "recovery_failed"]
|
|
595
|
+
plan: list[ResourceDeploymentPlan]
|
|
596
|
+
snapshot: SchemaSnapshot
|
|
597
|
+
responses: AppliedChanges | None = None
|
|
598
|
+
recovery: AppliedChanges | None = None
|
|
599
|
+
|
|
600
|
+
@property
|
|
601
|
+
def is_dry_run(self) -> bool:
|
|
602
|
+
return self.responses is None
|
|
603
|
+
|
|
604
|
+
@property
|
|
605
|
+
def is_success(self) -> bool:
|
|
606
|
+
return self.status in ("success", "pending")
|
|
607
|
+
|
|
608
|
+
def as_mixpanel_event(self) -> dict[str, Any]:
|
|
609
|
+
"""Convert deployment result to mixpanel event format"""
|
|
610
|
+
output: dict[str, Any] = {
|
|
611
|
+
"status": self.status,
|
|
612
|
+
"isDryRun": self.is_dry_run,
|
|
613
|
+
"isSuccess": self.is_success,
|
|
614
|
+
}
|
|
615
|
+
if self.responses:
|
|
616
|
+
counts: dict[str, int] = defaultdict(int)
|
|
617
|
+
for change in itertools.chain(self.responses.created, self.responses.updated, self.responses.deletions):
|
|
618
|
+
suffix = type(change.http_message).__name__.removesuffix("[TypeVar]").removesuffix("[~T_ResourceId]")
|
|
619
|
+
# For example: containers.created.successResponseItems
|
|
620
|
+
prefix = f"{change.endpoint}.{change.change.change_type}.{suffix}"
|
|
621
|
+
counts[prefix] += len(change.http_message.ids)
|
|
622
|
+
|
|
623
|
+
output.update(counts)
|
|
624
|
+
return output
|
|
625
|
+
|
|
626
|
+
|
|
627
|
+
def humanize_changes(changes: Sequence[FieldChange]) -> str:
|
|
628
|
+
primitive_changes = get_primitive_changes(changes)
|
|
629
|
+
lines = []
|
|
630
|
+
for change in primitive_changes:
|
|
631
|
+
lines.append(f"- Field '{change.field_path}': {change.description}")
|
|
632
|
+
return "\n".join(lines)
|
|
633
|
+
|
|
634
|
+
|
|
635
|
+
def get_primitive_changes(changes: Sequence[FieldChange]) -> list[PrimitiveField]:
|
|
636
|
+
primitive_changes: list[PrimitiveField] = []
|
|
637
|
+
for change in changes:
|
|
638
|
+
if isinstance(change, FieldChanges):
|
|
639
|
+
primitive_changes.extend(get_primitive_changes(change.changes))
|
|
640
|
+
elif isinstance(change, PrimitiveField):
|
|
641
|
+
primitive_changes.append(change)
|
|
642
|
+
else:
|
|
643
|
+
raise NotImplementedError(f"Unknown FieldChange type: {type(change)}")
|
|
644
|
+
return primitive_changes
|