cognite-neat 0.123.2__py3-none-any.whl → 0.127.30__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 +2 -2
- cognite/neat/_client/__init__.py +4 -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 +125 -0
- cognite/neat/_client/data_classes.py +44 -0
- cognite/neat/_client/data_model_api.py +115 -0
- cognite/neat/_client/spaces_api.py +115 -0
- cognite/neat/_client/statistics_api.py +24 -0
- cognite/neat/_client/views_api.py +129 -0
- cognite/neat/_config.py +185 -0
- cognite/neat/_data_model/_analysis.py +196 -0
- cognite/neat/_data_model/_constants.py +67 -0
- cognite/neat/_data_model/_identifiers.py +61 -0
- cognite/neat/_data_model/_shared.py +41 -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 +529 -0
- cognite/neat/_data_model/deployer/deployer.py +401 -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 +421 -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 +295 -0
- cognite/neat/_data_model/importers/_table_importer/importer.py +192 -0
- cognite/neat/_data_model/importers/_table_importer/reader.py +1063 -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 +135 -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 +282 -0
- cognite/neat/_data_model/models/dms/_view_property.py +235 -0
- cognite/neat/_data_model/models/dms/_views.py +210 -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 +364 -0
- cognite/neat/_data_model/validation/dms/_base.py +307 -0
- cognite/neat/_data_model/validation/dms/_connections.py +638 -0
- cognite/neat/_data_model/validation/dms/_consistency.py +57 -0
- cognite/neat/_data_model/validation/dms/_containers.py +174 -0
- cognite/neat/_data_model/validation/dms/_limits.py +420 -0
- cognite/neat/_data_model/validation/dms/_orchestrator.py +222 -0
- cognite/neat/_data_model/validation/dms/_views.py +103 -0
- cognite/neat/_exceptions.py +56 -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 +303 -0
- cognite/neat/_session/_html/static/deployment.js +150 -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 +75 -0
- cognite/neat/_session/_html/templates/issues.html +45 -0
- cognite/neat/_session/_issues.py +81 -0
- cognite/neat/_session/_opt.py +35 -0
- cognite/neat/_session/_physical.py +261 -0
- cognite/neat/_session/_result.py +236 -0
- cognite/neat/_session/_session.py +88 -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 +82 -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 +81 -0
- cognite/neat/_store/_store.py +156 -0
- cognite/neat/_utils/__init__.py +0 -0
- cognite/neat/_utils/_reader.py +194 -0
- cognite/neat/_utils/auxiliary.py +39 -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/text.py +71 -0
- cognite/neat/_utils/useful_types.py +37 -0
- cognite/neat/_utils/validation.py +154 -0
- cognite/neat/_version.py +1 -1
- 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 +86 -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 +10 -3
- cognite/neat/v0/core/_data_model/__init__.py +0 -0
- cognite/neat/{core → v0/core}/_data_model/_constants.py +9 -6
- cognite/neat/{core → v0/core}/_data_model/_shared.py +5 -5
- cognite/neat/{core → v0/core}/_data_model/analysis/_base.py +12 -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 +13 -13
- 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 -133
- cognite/neat/{core → v0/core}/_data_model/exporters/_data_model2yaml.py +1 -1
- cognite/neat/{core → v0/core}/_data_model/importers/__init__.py +4 -6
- 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 +6 -6
- cognite/neat/{core → v0/core}/_data_model/importers/_dms2data_model.py +19 -16
- cognite/neat/v0/core/_data_model/importers/_graph2data_model.py +299 -0
- cognite/neat/v0/core/_data_model/importers/_rdf/__init__.py +4 -0
- cognite/neat/{core → v0/core}/_data_model/importers/_rdf/_base.py +13 -13
- 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/v0/core/_data_model/importers/_rdf/_shared.py +255 -0
- cognite/neat/{core → v0/core}/_data_model/importers/_spreadsheet2data_model.py +94 -13
- 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/v0/core/_data_model/models/_import_contexts.py +82 -0
- cognite/neat/{core → v0/core}/_data_model/models/_types.py +5 -5
- cognite/neat/{core → v0/core}/_data_model/models/conceptual/_unverified.py +18 -12
- cognite/neat/v0/core/_data_model/models/conceptual/_validation.py +308 -0
- cognite/neat/{core → v0/core}/_data_model/models/conceptual/_verified.py +13 -11
- 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 +28 -21
- cognite/neat/{core → v0/core}/_data_model/models/physical/_unverified.py +141 -38
- cognite/neat/{core → v0/core}/_data_model/models/physical/_validation.py +190 -24
- cognite/neat/{core → v0/core}/_data_model/models/physical/_verified.py +135 -15
- cognite/neat/{core → v0/core}/_data_model/transformers/__init__.py +2 -0
- cognite/neat/{core → v0/core}/_data_model/transformers/_base.py +4 -4
- cognite/neat/{core → v0/core}/_data_model/transformers/_converters.py +39 -32
- cognite/neat/{core → v0/core}/_data_model/transformers/_mapping.py +7 -7
- cognite/neat/v0/core/_data_model/transformers/_union_conceptual.py +208 -0
- 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 +1 -1
- 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 +11 -6
- cognite/neat/{core → v0/core}/_issues/_contextmanagers.py +8 -6
- cognite/neat/{core → v0/core}/_issues/_factory.py +11 -8
- cognite/neat/{core → v0/core}/_issues/errors/__init__.py +3 -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 +12 -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 +5 -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 +39 -4
- cognite/neat/{core → v0/core}/_issues/warnings/_properties.py +13 -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 +13 -12
- cognite/neat/{core → v0/core}/_store/_instance.py +45 -12
- 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 +1 -1
- cognite/neat/{core → v0/core}/_utils/auxiliary.py +7 -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 +38 -14
- cognite/neat/{core → v0/core}/_utils/reader/_base.py +1 -1
- cognite/neat/{core → v0/core}/_utils/spreadsheet.py +22 -4
- cognite/neat/v0/core/_utils/tarjan.py +44 -0
- cognite/neat/{core → v0/core}/_utils/text.py +1 -1
- cognite/neat/{core → v0/core}/_utils/upload.py +3 -3
- 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 -10
- 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 +33 -43
- 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 +22 -8
- 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}/engine/_load.py +1 -1
- cognite/neat/{session → v0/session}/exceptions.py +5 -5
- cognite/neat/v1.py +3 -0
- {cognite_neat-0.123.2.dist-info → cognite_neat-0.127.30.dist-info}/METADATA +9 -8
- cognite_neat-0.127.30.dist-info/RECORD +319 -0
- {cognite_neat-0.123.2.dist-info → cognite_neat-0.127.30.dist-info}/WHEEL +1 -1
- cognite/neat/core/_data_model/importers/_dtdl2data_model/__init__.py +0 -3
- cognite/neat/core/_data_model/importers/_dtdl2data_model/_unit_lookup.py +0 -224
- cognite/neat/core/_data_model/importers/_dtdl2data_model/dtdl_converter.py +0 -320
- cognite/neat/core/_data_model/importers/_dtdl2data_model/dtdl_importer.py +0 -155
- cognite/neat/core/_data_model/importers/_dtdl2data_model/spec.py +0 -363
- cognite/neat/core/_data_model/importers/_rdf/__init__.py +0 -5
- cognite/neat/core/_data_model/importers/_rdf/_imf2data_model.py +0 -98
- cognite/neat/core/_data_model/importers/_rdf/_owl2data_model.py +0 -87
- cognite/neat/core/_data_model/importers/_rdf/_shared.py +0 -168
- cognite/neat/core/_data_model/models/conceptual/_validation.py +0 -294
- 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-0.123.2.dist-info/RECORD +0 -197
- /cognite/neat/{core → _data_model}/__init__.py +0 -0
- /cognite/neat/{core/_client/_api → _data_model/deployer}/__init__.py +0 -0
- /cognite/neat/{core/_client/data_classes → _data_model/exporters/_table_exporter}/__init__.py +0 -0
- /cognite/neat/{core/_data_model → _data_model/importers/_table_importer}/__init__.py +0 -0
- /cognite/neat/{core/_instances → _data_model/models}/__init__.py +0 -0
- /cognite/neat/{core/_instances/extractors/_classic_cdf → _data_model/models/conceptual}/__init__.py +0 -0
- /cognite/neat/{core/_utils → _data_model/validation}/__init__.py +0 -0
- /cognite/neat/{plugins/data_model → _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/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}/_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/time_.py +0 -0
- /cognite/neat/{core → v0/core}/_utils/xml_.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
- /cognite/neat/{session → v0/session}/engine/__init__.py +0 -0
- /cognite/neat/{session → v0/session}/engine/_import.py +0 -0
- /cognite/neat/{session → v0/session}/engine/_interface.py +0 -0
- {cognite_neat-0.123.2.dist-info → cognite_neat-0.127.30.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
from collections import UserList
|
|
2
|
+
from collections.abc import Callable
|
|
3
|
+
from datetime import datetime, timezone
|
|
4
|
+
from typing import Any, cast
|
|
5
|
+
|
|
6
|
+
from cognite.neat._data_model._shared import OnSuccess, OnSuccessIssuesChecker, OnSuccessResultProducer
|
|
7
|
+
from cognite.neat._data_model.deployer.data_classes import DeploymentResult
|
|
8
|
+
from cognite.neat._data_model.exporters import DMSExporter, DMSFileExporter
|
|
9
|
+
from cognite.neat._data_model.importers import DMSImporter, DMSTableImporter
|
|
10
|
+
from cognite.neat._data_model.models.dms import RequestSchema as PhysicalDataModel
|
|
11
|
+
from cognite.neat._exceptions import DataModelImportException
|
|
12
|
+
from cognite.neat._issues import IssueList
|
|
13
|
+
from cognite.neat._state_machine._states import EmptyState, PhysicalState, State
|
|
14
|
+
from cognite.neat._utils.text import NEWLINE
|
|
15
|
+
|
|
16
|
+
from ._provenance import Change, Provenance
|
|
17
|
+
|
|
18
|
+
Agents = DMSExporter | DMSTableImporter | DMSImporter
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class NeatStore:
|
|
22
|
+
def __init__(self) -> None:
|
|
23
|
+
self.physical_data_model = DataModelList()
|
|
24
|
+
self.provenance = Provenance()
|
|
25
|
+
self.state: State = EmptyState()
|
|
26
|
+
|
|
27
|
+
def read_physical(self, reader: DMSImporter, on_success: OnSuccess | None = None) -> None:
|
|
28
|
+
"""Read object from the store"""
|
|
29
|
+
self._can_agent_do_activity(reader)
|
|
30
|
+
|
|
31
|
+
change, data_model = self._do_activity(reader.to_data_model, on_success)
|
|
32
|
+
|
|
33
|
+
if data_model:
|
|
34
|
+
change.target_entity = self.physical_data_model.generate_reference(cast(PhysicalDataModel, data_model))
|
|
35
|
+
self.physical_data_model.append(data_model)
|
|
36
|
+
self.state = self.state.transition(reader)
|
|
37
|
+
change.target_state = self.state
|
|
38
|
+
|
|
39
|
+
self.provenance.append(change)
|
|
40
|
+
|
|
41
|
+
def write_physical(self, writer: DMSExporter, on_success: OnSuccess | None = None, **kwargs: Any) -> None:
|
|
42
|
+
"""Write object into the store"""
|
|
43
|
+
self._can_agent_do_activity(writer)
|
|
44
|
+
|
|
45
|
+
activity: Callable
|
|
46
|
+
if isinstance(writer, DMSFileExporter):
|
|
47
|
+
activity = writer.export_to_file
|
|
48
|
+
if not kwargs.get("file_path"):
|
|
49
|
+
raise RuntimeError("file_path must be provided when using a DMSFileExporter")
|
|
50
|
+
else:
|
|
51
|
+
activity = writer.export
|
|
52
|
+
|
|
53
|
+
change, _ = self._do_activity(activity, on_success, data_model=self.physical_data_model[-1], **kwargs)
|
|
54
|
+
|
|
55
|
+
if not change.issues:
|
|
56
|
+
change.target_entity = "ExternalEntity"
|
|
57
|
+
self.state = self.state.transition(writer)
|
|
58
|
+
change.target_state = self.state
|
|
59
|
+
|
|
60
|
+
self.provenance.append(change)
|
|
61
|
+
|
|
62
|
+
def _can_agent_do_activity(self, agent: Agents) -> None:
|
|
63
|
+
"""Validate if activity can be performed in the current state and considering provenance"""
|
|
64
|
+
if not self.state.can_transition(agent):
|
|
65
|
+
# specific error messages for common mistakes
|
|
66
|
+
if isinstance(agent, DMSImporter) and isinstance(self.state, PhysicalState):
|
|
67
|
+
raise RuntimeError(
|
|
68
|
+
"⚠️ Cannot read data model, there is already a data model in the session!"
|
|
69
|
+
f"{NEWLINE}Start a new session to read a new data model."
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
if isinstance(agent, DMSExporter) and isinstance(self.state, EmptyState):
|
|
73
|
+
raise RuntimeError(
|
|
74
|
+
"⚠️ Cannot write data model, there is no data model in the session!"
|
|
75
|
+
f"{NEWLINE}Read a data model first!"
|
|
76
|
+
)
|
|
77
|
+
raise RuntimeError(f"Cannot run {type(agent).__name__} in state {self.state}")
|
|
78
|
+
|
|
79
|
+
if (
|
|
80
|
+
isinstance(agent, DMSExporter)
|
|
81
|
+
and self.provenance.last_change
|
|
82
|
+
and (error_count := self.provenance.last_change.error_count) > 0
|
|
83
|
+
):
|
|
84
|
+
raise RuntimeError(
|
|
85
|
+
f"⚠️ Cannot write data model, the model has {error_count} errors!"
|
|
86
|
+
f"{NEWLINE}Resolve issues before exporting the data model."
|
|
87
|
+
f"{NEWLINE}You can inspect issues using neat.issues"
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
# need implementation of checking if required predecessor activities have been done
|
|
91
|
+
# this will be done by running self.provenance.can_agent_do_activity(agent)
|
|
92
|
+
|
|
93
|
+
def _do_activity(
|
|
94
|
+
self, activity: Callable, on_success: OnSuccess | None = None, **kwargs: Any
|
|
95
|
+
) -> tuple[Change, PhysicalDataModel | None]:
|
|
96
|
+
"""Execute activity and capture timing, results, and issues"""
|
|
97
|
+
start = datetime.now(timezone.utc)
|
|
98
|
+
created_data_model: PhysicalDataModel | None = None
|
|
99
|
+
issues = IssueList()
|
|
100
|
+
errors = IssueList()
|
|
101
|
+
deployment_result: DeploymentResult | None = None
|
|
102
|
+
|
|
103
|
+
try:
|
|
104
|
+
created_data_model = activity(**kwargs)
|
|
105
|
+
if created_data_model and on_success:
|
|
106
|
+
on_success.run(created_data_model)
|
|
107
|
+
if isinstance(on_success, OnSuccessIssuesChecker):
|
|
108
|
+
issues.extend(on_success.issues)
|
|
109
|
+
elif isinstance(on_success, OnSuccessResultProducer):
|
|
110
|
+
deployment_result = on_success.result
|
|
111
|
+
else:
|
|
112
|
+
raise RuntimeError(f"Unknown OnSuccess type {type(on_success).__name__}")
|
|
113
|
+
|
|
114
|
+
# we catch import exceptions to capture issues and errors in provenance
|
|
115
|
+
except DataModelImportException as e:
|
|
116
|
+
errors.extend(e.errors)
|
|
117
|
+
|
|
118
|
+
# these are all other errors, such as missing file, wrong format, etc.
|
|
119
|
+
except Exception as e:
|
|
120
|
+
raise e
|
|
121
|
+
|
|
122
|
+
end = datetime.now(timezone.utc)
|
|
123
|
+
|
|
124
|
+
return Change(
|
|
125
|
+
start=start,
|
|
126
|
+
end=end,
|
|
127
|
+
source_state=self.state,
|
|
128
|
+
agent=type(activity.__self__).__name__ if hasattr(activity, "__self__") else "UnknownAgent",
|
|
129
|
+
issues=issues,
|
|
130
|
+
errors=errors,
|
|
131
|
+
result=deployment_result,
|
|
132
|
+
activity=Change.standardize_activity_name(activity.__name__, start, end),
|
|
133
|
+
), created_data_model
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
class DataModelList(UserList[PhysicalDataModel]):
|
|
137
|
+
def iteration(self, data_model: PhysicalDataModel) -> int:
|
|
138
|
+
"""Get iteration number for data model"""
|
|
139
|
+
for i, existing in enumerate(self):
|
|
140
|
+
if existing.data_model == data_model.data_model:
|
|
141
|
+
return i + 2
|
|
142
|
+
return 1
|
|
143
|
+
|
|
144
|
+
def generate_reference(self, data_model: PhysicalDataModel) -> str:
|
|
145
|
+
"""Generate reference string for data model based on iteration"""
|
|
146
|
+
space = data_model.data_model.space
|
|
147
|
+
external_id = data_model.data_model.external_id
|
|
148
|
+
version = data_model.data_model.version
|
|
149
|
+
iteration = self.iteration(data_model)
|
|
150
|
+
|
|
151
|
+
return f"physical/{space}/{external_id}/{version}/{iteration}"
|
|
152
|
+
|
|
153
|
+
def get_by_reference(self, reference: str) -> PhysicalDataModel | None:
|
|
154
|
+
"""Get data model by reference string"""
|
|
155
|
+
|
|
156
|
+
raise NotImplementedError("Not implemented yet")
|
|
File without changes
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import tempfile
|
|
2
|
+
from abc import ABC, abstractmethod
|
|
3
|
+
from collections.abc import Iterable
|
|
4
|
+
from io import StringIO
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import IO, Any, TextIO
|
|
7
|
+
from urllib.parse import urlparse
|
|
8
|
+
|
|
9
|
+
import requests
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class NeatReader(ABC):
|
|
13
|
+
@classmethod
|
|
14
|
+
def create(cls, io: Any) -> "NeatReader":
|
|
15
|
+
if isinstance(io, str):
|
|
16
|
+
url = urlparse(io)
|
|
17
|
+
if url.scheme == "https" and url.netloc.endswith("github.com"):
|
|
18
|
+
return GitHubReader(io)
|
|
19
|
+
elif url.scheme == "https":
|
|
20
|
+
return HttpFileReader(io, url.path)
|
|
21
|
+
|
|
22
|
+
if isinstance(io, str):
|
|
23
|
+
return PathReader(Path(io))
|
|
24
|
+
if isinstance(io, Path):
|
|
25
|
+
return PathReader(io)
|
|
26
|
+
raise ValueError(f"Unsupported type: {type(io)}")
|
|
27
|
+
|
|
28
|
+
@property
|
|
29
|
+
def name(self) -> str:
|
|
30
|
+
return str(self)
|
|
31
|
+
|
|
32
|
+
@abstractmethod
|
|
33
|
+
def read_text(self) -> str:
|
|
34
|
+
"""Read the buffer as a string"""
|
|
35
|
+
raise NotImplementedError()
|
|
36
|
+
|
|
37
|
+
@abstractmethod
|
|
38
|
+
def read_bytes(self) -> bytes:
|
|
39
|
+
"""Read the buffer as bytes"""
|
|
40
|
+
raise NotImplementedError()
|
|
41
|
+
|
|
42
|
+
@abstractmethod
|
|
43
|
+
def size(self) -> int:
|
|
44
|
+
"""Size of the buffer in bytes"""
|
|
45
|
+
raise NotImplementedError()
|
|
46
|
+
|
|
47
|
+
@abstractmethod
|
|
48
|
+
def iterate(self, chunk_size: int) -> Iterable[str]:
|
|
49
|
+
"""Iterate over the buffer in chunks
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
chunk_size: Size of each chunk in bytes
|
|
53
|
+
"""
|
|
54
|
+
raise NotImplementedError()
|
|
55
|
+
|
|
56
|
+
@abstractmethod
|
|
57
|
+
def __enter__(self) -> IO:
|
|
58
|
+
raise NotImplementedError()
|
|
59
|
+
|
|
60
|
+
@abstractmethod
|
|
61
|
+
def __str__(self) -> str:
|
|
62
|
+
raise NotImplementedError()
|
|
63
|
+
|
|
64
|
+
@abstractmethod
|
|
65
|
+
def exists(self) -> bool:
|
|
66
|
+
raise NotImplementedError
|
|
67
|
+
|
|
68
|
+
@abstractmethod
|
|
69
|
+
def materialize_path(self) -> Path:
|
|
70
|
+
raise NotImplementedError
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class PathReader(NeatReader):
|
|
74
|
+
def __init__(self, path: Path):
|
|
75
|
+
self.path = path
|
|
76
|
+
self._io: TextIO | None = None
|
|
77
|
+
|
|
78
|
+
@property
|
|
79
|
+
def name(self) -> str:
|
|
80
|
+
return self.path.name
|
|
81
|
+
|
|
82
|
+
def read_text(self) -> str:
|
|
83
|
+
return self.path.read_text()
|
|
84
|
+
|
|
85
|
+
def read_bytes(self) -> bytes:
|
|
86
|
+
return self.path.read_bytes()
|
|
87
|
+
|
|
88
|
+
def size(self) -> int:
|
|
89
|
+
return self.path.stat().st_size
|
|
90
|
+
|
|
91
|
+
def iterate(self, chunk_size: int) -> Iterable[str]:
|
|
92
|
+
with self.path.open(mode="r") as f:
|
|
93
|
+
while chunk := f.read(chunk_size):
|
|
94
|
+
yield chunk
|
|
95
|
+
|
|
96
|
+
def __enter__(self) -> TextIO:
|
|
97
|
+
file = self.path.open(mode="r")
|
|
98
|
+
self._io = file
|
|
99
|
+
return file
|
|
100
|
+
|
|
101
|
+
def __exit__(self) -> None:
|
|
102
|
+
if self._io:
|
|
103
|
+
self._io.close()
|
|
104
|
+
|
|
105
|
+
def __str__(self) -> str:
|
|
106
|
+
return self.path.as_posix()
|
|
107
|
+
|
|
108
|
+
def exists(self) -> bool:
|
|
109
|
+
return self.path.exists()
|
|
110
|
+
|
|
111
|
+
def materialize_path(self) -> Path:
|
|
112
|
+
return self.path
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class HttpFileReader(NeatReader):
|
|
116
|
+
def __init__(self, url: str, path: str):
|
|
117
|
+
self._url = url
|
|
118
|
+
self.path = path
|
|
119
|
+
|
|
120
|
+
@property
|
|
121
|
+
def name(self) -> str:
|
|
122
|
+
if "/" in self.path:
|
|
123
|
+
return self.path.rsplit("/", maxsplit=1)[-1]
|
|
124
|
+
return self.path
|
|
125
|
+
|
|
126
|
+
def read_text(self) -> str:
|
|
127
|
+
response = requests.get(self._url)
|
|
128
|
+
response.raise_for_status()
|
|
129
|
+
return response.text
|
|
130
|
+
|
|
131
|
+
def read_bytes(self) -> bytes:
|
|
132
|
+
response = requests.get(self._url)
|
|
133
|
+
response.raise_for_status()
|
|
134
|
+
return response.content
|
|
135
|
+
|
|
136
|
+
def size(self) -> int:
|
|
137
|
+
response = requests.head(self._url)
|
|
138
|
+
response.raise_for_status()
|
|
139
|
+
return int(response.headers["Content-Length"])
|
|
140
|
+
|
|
141
|
+
def iterate(self, chunk_size: int) -> Iterable[str]:
|
|
142
|
+
with requests.get(self._url, stream=True) as response:
|
|
143
|
+
response.raise_for_status()
|
|
144
|
+
for chunk in response.iter_content(chunk_size):
|
|
145
|
+
yield chunk.decode("utf-8")
|
|
146
|
+
|
|
147
|
+
def __enter__(self) -> IO:
|
|
148
|
+
return StringIO(self.read_text())
|
|
149
|
+
|
|
150
|
+
def __str__(self) -> str:
|
|
151
|
+
return self._url
|
|
152
|
+
|
|
153
|
+
def exists(self) -> bool:
|
|
154
|
+
response = requests.head(self._url)
|
|
155
|
+
return 200 <= response.status_code < 400
|
|
156
|
+
|
|
157
|
+
def materialize_path(self) -> Path:
|
|
158
|
+
path = Path(tempfile.gettempdir()).resolve() / "neat" / self.name
|
|
159
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
160
|
+
path.write_bytes(self.read_bytes())
|
|
161
|
+
return path
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
class GitHubReader(HttpFileReader):
|
|
165
|
+
raw_url = "https://raw.githubusercontent.com/"
|
|
166
|
+
|
|
167
|
+
def __init__(self, raw: str):
|
|
168
|
+
self.raw = raw
|
|
169
|
+
self.repo, path = self._parse_url(raw)
|
|
170
|
+
super().__init__(f"{self.raw_url}{self.repo}/main/{path}", path)
|
|
171
|
+
|
|
172
|
+
@staticmethod
|
|
173
|
+
def _parse_url(url: str) -> tuple[str, str]:
|
|
174
|
+
parsed = urlparse(url)
|
|
175
|
+
if parsed.scheme != "https":
|
|
176
|
+
raise ValueError(f"Unsupported scheme: {parsed.scheme}")
|
|
177
|
+
|
|
178
|
+
path = parsed.path.lstrip("/")
|
|
179
|
+
if parsed.netloc == "github.com":
|
|
180
|
+
repo, path = path.split("/blob/main/", maxsplit=1)
|
|
181
|
+
return repo, path
|
|
182
|
+
|
|
183
|
+
elif parsed.netloc == "api.github.com":
|
|
184
|
+
repo, path = path.removeprefix("repos/").split("/contents/", maxsplit=1)
|
|
185
|
+
return repo, path
|
|
186
|
+
|
|
187
|
+
elif parsed.netloc == "raw.githubusercontent.com":
|
|
188
|
+
repo, path = path.split("/main/", maxsplit=1)
|
|
189
|
+
return repo, path
|
|
190
|
+
|
|
191
|
+
raise ValueError(f"Unsupported netloc: {parsed.netloc}")
|
|
192
|
+
|
|
193
|
+
def __str__(self) -> str:
|
|
194
|
+
return self.raw
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import inspect
|
|
2
|
+
from abc import ABC
|
|
3
|
+
from typing import TypeVar
|
|
4
|
+
|
|
5
|
+
from cognite.neat import _version
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def get_current_neat_version() -> str:
|
|
9
|
+
return _version.__version__
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
T_Cls = TypeVar("T_Cls")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def get_concrete_subclasses(base_cls: type[T_Cls], exclude_direct_abc_inheritance: bool = True) -> list[type[T_Cls]]:
|
|
16
|
+
"""
|
|
17
|
+
Returns a list of all concrete subclasses of the given base class.
|
|
18
|
+
Args:
|
|
19
|
+
base_cls (type[T_Cls]): The base class to find subclasses for.
|
|
20
|
+
exclude_direct_abc_inheritance (bool): If True, excludes classes that directly inherit from `abc.ABC`.
|
|
21
|
+
This is used as a marker to filter out intermediate base classes. Defaults to True.
|
|
22
|
+
Returns:
|
|
23
|
+
list[type[T_Cls]]: A list of concrete subclasses of the base class.
|
|
24
|
+
"""
|
|
25
|
+
to_check = [base_cls]
|
|
26
|
+
subclasses: list[type[T_Cls]] = []
|
|
27
|
+
seen: set[type[T_Cls]] = {base_cls}
|
|
28
|
+
while to_check:
|
|
29
|
+
current_cls = to_check.pop()
|
|
30
|
+
for subclass in current_cls.__subclasses__():
|
|
31
|
+
if subclass in seen:
|
|
32
|
+
continue
|
|
33
|
+
if (not inspect.isabstract(subclass)) and (
|
|
34
|
+
not exclude_direct_abc_inheritance or ABC not in subclass.__bases__
|
|
35
|
+
):
|
|
36
|
+
subclasses.append(subclass)
|
|
37
|
+
seen.add(subclass)
|
|
38
|
+
to_check.append(subclass)
|
|
39
|
+
return subclasses
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from collections.abc import Iterator, Sequence
|
|
2
|
+
from typing import TypeVar
|
|
3
|
+
|
|
4
|
+
T_Sequence = TypeVar("T_Sequence", bound=Sequence)
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def chunker_sequence(sequence: T_Sequence, size: int) -> Iterator[T_Sequence]:
|
|
8
|
+
"""Yield successive n-sized chunks from sequence."""
|
|
9
|
+
for i in range(0, len(sequence), size):
|
|
10
|
+
# MyPy does not expect sequence[i : i + size] to be of type T_Sequence
|
|
11
|
+
yield sequence[i : i + size] # type: ignore[misc]
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
from ._client import HTTPClient
|
|
2
|
+
from ._data_classes import (
|
|
3
|
+
ErrorDetails,
|
|
4
|
+
FailedRequestItems,
|
|
5
|
+
FailedRequestMessage,
|
|
6
|
+
FailedResponse,
|
|
7
|
+
FailedResponseItems,
|
|
8
|
+
HTTPMessage,
|
|
9
|
+
ItemBody,
|
|
10
|
+
ItemIDBody,
|
|
11
|
+
ItemMessage,
|
|
12
|
+
ItemsRequest,
|
|
13
|
+
ParametersRequest,
|
|
14
|
+
RequestMessage,
|
|
15
|
+
ResponseMessage,
|
|
16
|
+
SimpleBodyRequest,
|
|
17
|
+
SuccessResponse,
|
|
18
|
+
SuccessResponseItems,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
__all__ = [
|
|
22
|
+
"ErrorDetails",
|
|
23
|
+
"FailedRequestItems",
|
|
24
|
+
"FailedRequestMessage",
|
|
25
|
+
"FailedResponse",
|
|
26
|
+
"FailedResponseItems",
|
|
27
|
+
"HTTPClient",
|
|
28
|
+
"HTTPMessage",
|
|
29
|
+
"ItemBody",
|
|
30
|
+
"ItemIDBody",
|
|
31
|
+
"ItemMessage",
|
|
32
|
+
"ItemsRequest",
|
|
33
|
+
"ParametersRequest",
|
|
34
|
+
"RequestMessage",
|
|
35
|
+
"ResponseMessage",
|
|
36
|
+
"SimpleBodyRequest",
|
|
37
|
+
"SuccessResponse",
|
|
38
|
+
"SuccessResponseItems",
|
|
39
|
+
]
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import gzip
|
|
2
|
+
import random
|
|
3
|
+
import sys
|
|
4
|
+
import time
|
|
5
|
+
from collections import deque
|
|
6
|
+
from collections.abc import MutableMapping, Sequence, Set
|
|
7
|
+
from typing import Literal
|
|
8
|
+
|
|
9
|
+
import httpx
|
|
10
|
+
from cognite.client import ClientConfig, global_config
|
|
11
|
+
|
|
12
|
+
from cognite.neat._utils.auxiliary import get_current_neat_version
|
|
13
|
+
from cognite.neat._utils.http_client._config import get_user_agent
|
|
14
|
+
from cognite.neat._utils.http_client._data_classes import (
|
|
15
|
+
APIResponse,
|
|
16
|
+
BodyRequest,
|
|
17
|
+
ErrorDetails,
|
|
18
|
+
FailedRequestMessage,
|
|
19
|
+
HTTPMessage,
|
|
20
|
+
ItemsRequest,
|
|
21
|
+
ParametersRequest,
|
|
22
|
+
RequestMessage,
|
|
23
|
+
ResponseMessage,
|
|
24
|
+
)
|
|
25
|
+
from cognite.neat._utils.useful_types import PrimaryTypes
|
|
26
|
+
|
|
27
|
+
if sys.version_info >= (3, 11):
|
|
28
|
+
from typing import Self
|
|
29
|
+
else:
|
|
30
|
+
from typing_extensions import Self
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class HTTPClient:
|
|
34
|
+
"""An HTTP client.
|
|
35
|
+
|
|
36
|
+
This class handles rate limiting, retries, and error handling for HTTP requests.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
config (ClientConfig): Configuration for the client.
|
|
40
|
+
pool_connections (int): The number of connection pools to cache. Default is 10.
|
|
41
|
+
pool_maxsize (int): The maximum number of connections to save in the pool. Default
|
|
42
|
+
is 20.
|
|
43
|
+
max_retries (int): The maximum number of retries for a request. Default is 10.
|
|
44
|
+
retry_status_codes (frozenset[int]): HTTP status codes that should trigger a retry.
|
|
45
|
+
Default is {408, 429, 502, 503, 504}.
|
|
46
|
+
split_items_status_codes (frozenset[int]): In the case of ItemRequest with multiple
|
|
47
|
+
items, these status codes will trigger splitting the request into smaller batches.
|
|
48
|
+
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
def __init__(
|
|
52
|
+
self,
|
|
53
|
+
config: ClientConfig,
|
|
54
|
+
max_retries: int = 10,
|
|
55
|
+
pool_connections: int = 10,
|
|
56
|
+
pool_maxsize: int = 20,
|
|
57
|
+
retry_status_codes: Set[int] = frozenset({429, 502, 503, 504}),
|
|
58
|
+
split_items_status_codes: Set[int] = frozenset({400, 408, 409, 422, 502, 503, 504}),
|
|
59
|
+
):
|
|
60
|
+
self.config = config
|
|
61
|
+
self._max_retries = max_retries
|
|
62
|
+
self._pool_connections = pool_connections
|
|
63
|
+
self._pool_maxsize = pool_maxsize
|
|
64
|
+
self._retry_status_codes = retry_status_codes
|
|
65
|
+
self._split_items_status_codes = split_items_status_codes
|
|
66
|
+
|
|
67
|
+
# Thread-safe session for connection pooling
|
|
68
|
+
self.session = self._create_thread_safe_session()
|
|
69
|
+
|
|
70
|
+
def __enter__(self) -> Self:
|
|
71
|
+
return self
|
|
72
|
+
|
|
73
|
+
def __exit__(
|
|
74
|
+
self, exc_type: type[BaseException] | None, exc_value: BaseException | None, traceback: object | None
|
|
75
|
+
) -> Literal[False]:
|
|
76
|
+
"""Close the session when exiting the context."""
|
|
77
|
+
self.session.close()
|
|
78
|
+
return False # Do not suppress exceptions
|
|
79
|
+
|
|
80
|
+
def request(self, message: RequestMessage) -> Sequence[HTTPMessage]:
|
|
81
|
+
"""Send an HTTP request and return the response.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
message (RequestMessage): The request message to send.
|
|
85
|
+
|
|
86
|
+
Returns:
|
|
87
|
+
Sequence[HTTPMessage]: The response message(s). This can also
|
|
88
|
+
include RequestMessage(s) to be retried.
|
|
89
|
+
"""
|
|
90
|
+
if isinstance(message, ItemsRequest) and message.tracker and message.tracker.limit_reached():
|
|
91
|
+
error_msg = (
|
|
92
|
+
f"Aborting further splitting of requests after {message.tracker.failed_split_count} failed attempts."
|
|
93
|
+
)
|
|
94
|
+
return message.create_failed_request(error_msg)
|
|
95
|
+
try:
|
|
96
|
+
response = self._make_request(message)
|
|
97
|
+
results = self._handle_response(response, message)
|
|
98
|
+
except Exception as e:
|
|
99
|
+
results = self._handle_error(e, message)
|
|
100
|
+
return results
|
|
101
|
+
|
|
102
|
+
def request_with_retries(self, message: RequestMessage) -> APIResponse:
|
|
103
|
+
"""Send an HTTP request and handle retries.
|
|
104
|
+
|
|
105
|
+
This method will keep retrying the request until it either succeeds or
|
|
106
|
+
exhausts the maximum number of retries.
|
|
107
|
+
|
|
108
|
+
Note this method will use the current thread to process all request, thus
|
|
109
|
+
it is blocking.
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
message (RequestMessage): The request message to send.
|
|
113
|
+
|
|
114
|
+
Returns:
|
|
115
|
+
Sequence[ResponseMessage | FailedRequestMessage]: The final response
|
|
116
|
+
messages, which can be either successful responses or failed requests.
|
|
117
|
+
"""
|
|
118
|
+
if message.total_attempts > 0:
|
|
119
|
+
raise RuntimeError(f"RequestMessage has already been attempted {message.total_attempts} times.")
|
|
120
|
+
pending_requests: deque[RequestMessage] = deque()
|
|
121
|
+
pending_requests.append(message)
|
|
122
|
+
final_responses = APIResponse()
|
|
123
|
+
|
|
124
|
+
while pending_requests:
|
|
125
|
+
current_request = pending_requests.popleft()
|
|
126
|
+
results = self.request(current_request)
|
|
127
|
+
|
|
128
|
+
for result in results:
|
|
129
|
+
if isinstance(result, RequestMessage):
|
|
130
|
+
pending_requests.append(result)
|
|
131
|
+
elif isinstance(result, ResponseMessage | FailedRequestMessage):
|
|
132
|
+
final_responses.append(result)
|
|
133
|
+
else:
|
|
134
|
+
raise TypeError(f"Unexpected result type: {type(result)}")
|
|
135
|
+
|
|
136
|
+
return final_responses
|
|
137
|
+
|
|
138
|
+
def _create_thread_safe_session(self) -> httpx.Client:
|
|
139
|
+
return httpx.Client(
|
|
140
|
+
limits=httpx.Limits(
|
|
141
|
+
max_connections=self._pool_maxsize,
|
|
142
|
+
max_keepalive_connections=self._pool_connections,
|
|
143
|
+
),
|
|
144
|
+
timeout=self.config.timeout,
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
def _create_headers(self, api_version: str | None = None) -> MutableMapping[str, str]:
|
|
148
|
+
headers: MutableMapping[str, str] = {}
|
|
149
|
+
headers["User-Agent"] = f"httpx/{httpx.__version__} {get_user_agent()}"
|
|
150
|
+
auth_name, auth_value = self.config.credentials.authorization_header()
|
|
151
|
+
headers[auth_name] = auth_value
|
|
152
|
+
headers["content-type"] = "application/json"
|
|
153
|
+
headers["accept"] = "application/json"
|
|
154
|
+
headers["x-cdp-sdk"] = f"CogniteNeat:{get_current_neat_version()}"
|
|
155
|
+
headers["x-cdp-app"] = self.config.client_name
|
|
156
|
+
headers["cdf-version"] = api_version or self.config.api_subversion
|
|
157
|
+
if not global_config.disable_gzip:
|
|
158
|
+
headers["Content-Encoding"] = "gzip"
|
|
159
|
+
return headers
|
|
160
|
+
|
|
161
|
+
def _make_request(self, item: RequestMessage) -> httpx.Response:
|
|
162
|
+
headers = self._create_headers(item.api_version)
|
|
163
|
+
params: dict[str, PrimaryTypes] | None = None
|
|
164
|
+
if isinstance(item, ParametersRequest):
|
|
165
|
+
params = item.parameters
|
|
166
|
+
data: str | bytes | None = None
|
|
167
|
+
if isinstance(item, BodyRequest):
|
|
168
|
+
data = item.data()
|
|
169
|
+
if not global_config.disable_gzip:
|
|
170
|
+
data = gzip.compress(data.encode("utf-8"))
|
|
171
|
+
return self.session.request(
|
|
172
|
+
method=item.method,
|
|
173
|
+
url=item.endpoint_url,
|
|
174
|
+
content=data,
|
|
175
|
+
headers=headers,
|
|
176
|
+
params=params,
|
|
177
|
+
timeout=self.config.timeout,
|
|
178
|
+
follow_redirects=False,
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
def _handle_response(
|
|
182
|
+
self,
|
|
183
|
+
response: httpx.Response,
|
|
184
|
+
request: RequestMessage,
|
|
185
|
+
) -> Sequence[HTTPMessage]:
|
|
186
|
+
if 200 <= response.status_code < 300:
|
|
187
|
+
return request.create_success_response(response)
|
|
188
|
+
|
|
189
|
+
if (
|
|
190
|
+
isinstance(request, ItemsRequest)
|
|
191
|
+
and len(request.body.items) > 1
|
|
192
|
+
and response.status_code in self._split_items_status_codes
|
|
193
|
+
):
|
|
194
|
+
# 4XX: Status there is at least one item that is invalid, split the batch to get all valid items processed
|
|
195
|
+
# 5xx: Server error, split to reduce the number of items in each request, and count as a status attempt
|
|
196
|
+
status_attempts = request.status_attempt
|
|
197
|
+
if 500 <= response.status_code < 600:
|
|
198
|
+
status_attempts += 1
|
|
199
|
+
splits = request.split(status_attempts=status_attempts)
|
|
200
|
+
if splits[0].tracker and splits[0].tracker.limit_reached():
|
|
201
|
+
return request.create_failure_response(response)
|
|
202
|
+
return splits
|
|
203
|
+
|
|
204
|
+
error = ErrorDetails.from_response(response)
|
|
205
|
+
|
|
206
|
+
if request.status_attempt < self._max_retries and (
|
|
207
|
+
response.status_code in self._retry_status_codes or error.is_auto_retryable
|
|
208
|
+
):
|
|
209
|
+
request.status_attempt += 1
|
|
210
|
+
time.sleep(self._backoff_time(request.total_attempts))
|
|
211
|
+
return [request]
|
|
212
|
+
else:
|
|
213
|
+
# Permanent failure
|
|
214
|
+
return request.create_failure_response(response)
|
|
215
|
+
|
|
216
|
+
@staticmethod
|
|
217
|
+
def _backoff_time(attempts: int) -> float:
|
|
218
|
+
backoff_time = 0.5 * (2**attempts)
|
|
219
|
+
return min(backoff_time, global_config.max_retry_backoff) * random.uniform(0, 1.0)
|
|
220
|
+
|
|
221
|
+
def _handle_error(
|
|
222
|
+
self,
|
|
223
|
+
e: Exception,
|
|
224
|
+
request: RequestMessage,
|
|
225
|
+
) -> Sequence[HTTPMessage]:
|
|
226
|
+
if isinstance(e, httpx.ReadTimeout | httpx.TimeoutException):
|
|
227
|
+
error_type = "read"
|
|
228
|
+
request.read_attempt += 1
|
|
229
|
+
attempts = request.read_attempt
|
|
230
|
+
elif isinstance(e, ConnectionError | httpx.ConnectError | httpx.ConnectTimeout):
|
|
231
|
+
error_type = "connect"
|
|
232
|
+
request.connect_attempt += 1
|
|
233
|
+
attempts = request.connect_attempt
|
|
234
|
+
else:
|
|
235
|
+
error_msg = f"Unexpected exception: {e!s}"
|
|
236
|
+
return request.create_failed_request(error_msg)
|
|
237
|
+
|
|
238
|
+
if attempts <= self._max_retries:
|
|
239
|
+
time.sleep(self._backoff_time(request.total_attempts))
|
|
240
|
+
return [request]
|
|
241
|
+
else:
|
|
242
|
+
# We have already incremented the attempt count, so we subtract 1 here
|
|
243
|
+
error_msg = f"RequestException after {request.total_attempts - 1} attempts ({error_type} error): {e!s}"
|
|
244
|
+
|
|
245
|
+
return request.create_failed_request(error_msg)
|