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,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)
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import functools
|
|
2
|
+
import platform
|
|
3
|
+
|
|
4
|
+
from cognite.neat._utils.auxiliary import get_current_neat_version
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@functools.lru_cache(maxsize=1)
|
|
8
|
+
def get_user_agent() -> str:
|
|
9
|
+
neat_version = f"CogniteNeat/{get_current_neat_version()}"
|
|
10
|
+
python_version = (
|
|
11
|
+
f"{platform.python_implementation()}/{platform.python_version()} "
|
|
12
|
+
f"({platform.python_build()};{platform.python_compiler()})"
|
|
13
|
+
)
|
|
14
|
+
os_version_info = [platform.release(), platform.machine(), platform.architecture()[0]]
|
|
15
|
+
os_version_info = [s for s in os_version_info if s] # Ignore empty strings
|
|
16
|
+
os_version_info_str = "-".join(os_version_info)
|
|
17
|
+
operating_system = f"{platform.system()}/{os_version_info_str}"
|
|
18
|
+
|
|
19
|
+
return f"{neat_version} {python_version} {operating_system}"
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
from abc import ABC, abstractmethod
|
|
3
|
+
from collections import UserList
|
|
4
|
+
from collections.abc import MutableSequence, Sequence
|
|
5
|
+
from typing import Generic, Literal, TypeAlias, TypeVar
|
|
6
|
+
|
|
7
|
+
import httpx
|
|
8
|
+
from pydantic import BaseModel, ConfigDict, Field, JsonValue, ValidationError, model_serializer
|
|
9
|
+
|
|
10
|
+
from cognite.neat._exceptions import CDFAPIException
|
|
11
|
+
from cognite.neat._utils.http_client._tracker import ItemsRequestTracker
|
|
12
|
+
from cognite.neat._utils.useful_types import PrimaryTypes, ReferenceObject, T_Reference
|
|
13
|
+
|
|
14
|
+
if sys.version_info >= (3, 11):
|
|
15
|
+
from typing import Self
|
|
16
|
+
else:
|
|
17
|
+
from typing_extensions import Self
|
|
18
|
+
|
|
19
|
+
StatusCode: TypeAlias = int
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class HTTPMessage(BaseModel):
|
|
23
|
+
"""Base class for HTTP messages (requests and responses)"""
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class FailedRequestMessage(HTTPMessage):
|
|
27
|
+
message: str
|
|
28
|
+
|
|
29
|
+
def __str__(self) -> str:
|
|
30
|
+
return self.message
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class ResponseMessage(HTTPMessage):
|
|
34
|
+
code: StatusCode
|
|
35
|
+
body: str
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class SuccessResponse(ResponseMessage): ...
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class ErrorDetails(BaseModel):
|
|
42
|
+
"""This is the structure of failure responses from CDF APIs"""
|
|
43
|
+
|
|
44
|
+
code: StatusCode
|
|
45
|
+
message: str
|
|
46
|
+
missing: list[JsonValue] | None = None
|
|
47
|
+
duplicated: list[JsonValue] | None = None
|
|
48
|
+
is_auto_retryable: bool | None = Field(None, alias="isAutoRetryable")
|
|
49
|
+
|
|
50
|
+
@classmethod
|
|
51
|
+
def from_response(cls, response: httpx.Response) -> "ErrorDetails":
|
|
52
|
+
try:
|
|
53
|
+
return _ErrorResponse.model_validate_json(response.text).error
|
|
54
|
+
except ValidationError:
|
|
55
|
+
return cls(code=response.status_code, message=response.text)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class _ErrorResponse(BaseModel):
|
|
59
|
+
error: ErrorDetails
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class FailedResponse(ResponseMessage):
|
|
63
|
+
error: ErrorDetails
|
|
64
|
+
|
|
65
|
+
def __str__(self) -> str:
|
|
66
|
+
return f"HTTP {self.code} | {self.error.message}"
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class RequestMessage(HTTPMessage, ABC):
|
|
70
|
+
"""Base class for HTTP request messages"""
|
|
71
|
+
|
|
72
|
+
endpoint_url: str
|
|
73
|
+
method: Literal["GET", "POST", "PATCH", "DELETE"]
|
|
74
|
+
connect_attempt: int = 0
|
|
75
|
+
read_attempt: int = 0
|
|
76
|
+
status_attempt: int = 0
|
|
77
|
+
api_version: str | None = None
|
|
78
|
+
|
|
79
|
+
@property
|
|
80
|
+
def total_attempts(self) -> int:
|
|
81
|
+
return self.connect_attempt + self.read_attempt + self.status_attempt
|
|
82
|
+
|
|
83
|
+
@abstractmethod
|
|
84
|
+
def create_success_response(self, response: httpx.Response) -> Sequence[HTTPMessage]:
|
|
85
|
+
raise NotImplementedError()
|
|
86
|
+
|
|
87
|
+
@abstractmethod
|
|
88
|
+
def create_failure_response(self, response: httpx.Response) -> Sequence[HTTPMessage]:
|
|
89
|
+
raise NotImplementedError()
|
|
90
|
+
|
|
91
|
+
@abstractmethod
|
|
92
|
+
def create_failed_request(self, error_message: str) -> Sequence[HTTPMessage]:
|
|
93
|
+
raise NotImplementedError()
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class SimpleRequest(RequestMessage):
|
|
97
|
+
"""Base class for requests with a simple success/fail response structure"""
|
|
98
|
+
|
|
99
|
+
def create_success_response(self, response: httpx.Response) -> Sequence[HTTPMessage]:
|
|
100
|
+
return [SuccessResponse(code=response.status_code, body=response.text)]
|
|
101
|
+
|
|
102
|
+
def create_failure_response(self, response: httpx.Response) -> Sequence[HTTPMessage]:
|
|
103
|
+
return [
|
|
104
|
+
FailedResponse(code=response.status_code, body=response.text, error=ErrorDetails.from_response(response))
|
|
105
|
+
]
|
|
106
|
+
|
|
107
|
+
def create_failed_request(self, error_message: str) -> Sequence[HTTPMessage]:
|
|
108
|
+
return [FailedRequestMessage(message=error_message)]
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
class ParametersRequest(SimpleRequest):
|
|
112
|
+
"""Base class for HTTP request messages with query parameters"""
|
|
113
|
+
|
|
114
|
+
parameters: dict[str, PrimaryTypes] | None = None
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
class BodyRequest(ParametersRequest, ABC):
|
|
118
|
+
"""Base class for HTTP request messages with a body"""
|
|
119
|
+
|
|
120
|
+
@abstractmethod
|
|
121
|
+
def data(self) -> str:
|
|
122
|
+
raise NotImplementedError()
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
class SimpleBodyRequest(BodyRequest):
|
|
126
|
+
body: str
|
|
127
|
+
|
|
128
|
+
def data(self) -> str:
|
|
129
|
+
return self.body
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
class ItemMessage(BaseModel, Generic[T_Reference], ABC):
|
|
133
|
+
"""Base class for message related to a specific item"""
|
|
134
|
+
|
|
135
|
+
ids: Sequence[T_Reference]
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
class SuccessResponseItems(ItemMessage[T_Reference], SuccessResponse): ...
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
class FailedResponseItems(ItemMessage[T_Reference], FailedResponse): ...
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
class FailedRequestItems(ItemMessage[T_Reference], FailedRequestMessage): ...
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
T_BaseModel = TypeVar("T_BaseModel", bound=BaseModel)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
class ItemBody(BaseModel, Generic[T_Reference, T_BaseModel], ABC):
|
|
151
|
+
items: Sequence[T_BaseModel]
|
|
152
|
+
extra_args: dict[str, JsonValue] | None = None
|
|
153
|
+
|
|
154
|
+
@model_serializer(mode="plain", return_type=dict)
|
|
155
|
+
def serialize(self) -> dict[str, JsonValue]:
|
|
156
|
+
data: dict[str, JsonValue] = {
|
|
157
|
+
"items": [item.model_dump(exclude_unset=False, by_alias=True, exclude_none=False) for item in self.items]
|
|
158
|
+
}
|
|
159
|
+
if isinstance(self.extra_args, dict):
|
|
160
|
+
data.update(self.extra_args)
|
|
161
|
+
return data
|
|
162
|
+
|
|
163
|
+
@abstractmethod
|
|
164
|
+
def as_ids(self) -> list[T_Reference]:
|
|
165
|
+
"""Returns the list of item identifiers for the items in the body."""
|
|
166
|
+
raise NotImplementedError()
|
|
167
|
+
|
|
168
|
+
def split(self, mid: int) -> tuple[Self, Self]:
|
|
169
|
+
"""Splits the body into two smaller bodies.
|
|
170
|
+
|
|
171
|
+
This is useful for retrying requests that fail due to size limits or timeouts.
|
|
172
|
+
|
|
173
|
+
Args:
|
|
174
|
+
mid: The index at which to split the items.
|
|
175
|
+
Returns:
|
|
176
|
+
A tuple containing two new ItemBody instances, each with half of the original items.
|
|
177
|
+
|
|
178
|
+
"""
|
|
179
|
+
|
|
180
|
+
type_ = type(self)
|
|
181
|
+
return type_(items=self.items[:mid], extra_args=self.extra_args), type_(
|
|
182
|
+
items=self.items[mid:], extra_args=self.extra_args
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
class ItemIDBody(ItemBody[ReferenceObject, ReferenceObject]):
|
|
187
|
+
def as_ids(self) -> list[ReferenceObject]:
|
|
188
|
+
return list(self.items)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
class ItemsRequest(BodyRequest, Generic[T_Reference, T_BaseModel]):
|
|
192
|
+
"""Requests message for endpoints that accept multiple items in a single request.
|
|
193
|
+
|
|
194
|
+
This class provides functionality to split large requests into smaller ones, handle responses for each item,
|
|
195
|
+
and manage errors effectively.
|
|
196
|
+
|
|
197
|
+
Attributes:
|
|
198
|
+
body (ItemBody): The body of the request containing the items to be processed.
|
|
199
|
+
max_failures_before_abort (int): The maximum number of failed split requests before aborting further splits.
|
|
200
|
+
|
|
201
|
+
"""
|
|
202
|
+
|
|
203
|
+
model_config = ConfigDict(arbitrary_types_allowed=True)
|
|
204
|
+
body: ItemBody[T_Reference, T_BaseModel]
|
|
205
|
+
max_failures_before_abort: int = 50
|
|
206
|
+
tracker: ItemsRequestTracker | None = None
|
|
207
|
+
|
|
208
|
+
def data(self) -> str:
|
|
209
|
+
return self.body.model_dump_json(exclude_unset=True, by_alias=True)
|
|
210
|
+
|
|
211
|
+
def split(self, status_attempts: int) -> "list[ItemsRequest]":
|
|
212
|
+
"""Splits the request into two smaller requests.
|
|
213
|
+
|
|
214
|
+
This is useful for retrying requests that fail due to size limits or timeouts.
|
|
215
|
+
|
|
216
|
+
Args:
|
|
217
|
+
status_attempts: The number of status attempts to set for the new requests. This is used when the
|
|
218
|
+
request failed with a 5xx status code and we want to track the number of attempts. For 4xx errors,
|
|
219
|
+
there is at least one item causing the error, so we do not increment the status attempts, but
|
|
220
|
+
instead essentially do a binary search to find the problematic item(s).
|
|
221
|
+
|
|
222
|
+
Returns:
|
|
223
|
+
A list containing two new ItemsRequest instances, each with half of the original items.
|
|
224
|
+
|
|
225
|
+
"""
|
|
226
|
+
mid = len(self.body.items) // 2
|
|
227
|
+
if mid == 0:
|
|
228
|
+
return [self]
|
|
229
|
+
tracker = self.tracker or ItemsRequestTracker(self.max_failures_before_abort)
|
|
230
|
+
tracker.register_failure()
|
|
231
|
+
messages: list[ItemsRequest] = []
|
|
232
|
+
for body in self.body.split(mid):
|
|
233
|
+
item_request = ItemsRequest(
|
|
234
|
+
endpoint_url=self.endpoint_url,
|
|
235
|
+
method=self.method,
|
|
236
|
+
body=body,
|
|
237
|
+
connect_attempt=self.connect_attempt,
|
|
238
|
+
read_attempt=self.read_attempt,
|
|
239
|
+
status_attempt=status_attempts,
|
|
240
|
+
)
|
|
241
|
+
item_request.tracker = tracker
|
|
242
|
+
messages.append(item_request)
|
|
243
|
+
return messages
|
|
244
|
+
|
|
245
|
+
def create_success_response(self, response: httpx.Response) -> Sequence[HTTPMessage]:
|
|
246
|
+
return [SuccessResponseItems(code=response.status_code, body=response.text, ids=self.body.as_ids())]
|
|
247
|
+
|
|
248
|
+
def create_failure_response(self, response: httpx.Response) -> Sequence[HTTPMessage]:
|
|
249
|
+
"""Creates response messages based on the HTTP response and the original request.
|
|
250
|
+
|
|
251
|
+
Args:
|
|
252
|
+
response: The HTTP response received from the server.
|
|
253
|
+
Returns:
|
|
254
|
+
A sequence of HTTPMessage instances representing the outcome for each item in the request.
|
|
255
|
+
"""
|
|
256
|
+
return [
|
|
257
|
+
FailedResponseItems(
|
|
258
|
+
code=response.status_code,
|
|
259
|
+
body=response.text,
|
|
260
|
+
error=ErrorDetails.from_response(response),
|
|
261
|
+
ids=self.body.as_ids(),
|
|
262
|
+
)
|
|
263
|
+
]
|
|
264
|
+
|
|
265
|
+
def create_failed_request(self, error_message: str) -> Sequence[HTTPMessage]:
|
|
266
|
+
"""Creates failed request messages for each item in the request.
|
|
267
|
+
|
|
268
|
+
Args:
|
|
269
|
+
error_message: The error message to include in the failed request messages.
|
|
270
|
+
|
|
271
|
+
Returns:
|
|
272
|
+
A sequence of HTTPMessage instances representing the failed request for each item.
|
|
273
|
+
"""
|
|
274
|
+
return [FailedRequestItems(message=error_message, ids=self.body.as_ids())]
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
class APIResponse(UserList, MutableSequence[ResponseMessage | FailedRequestMessage]):
|
|
278
|
+
def __init__(self, collection: Sequence[ResponseMessage | FailedRequestMessage] | None = None):
|
|
279
|
+
super().__init__(collection or [])
|
|
280
|
+
|
|
281
|
+
def raise_for_status(self) -> None:
|
|
282
|
+
error_messages = [message for message in self.data if not isinstance(message, SuccessResponse)]
|
|
283
|
+
if error_messages:
|
|
284
|
+
raise CDFAPIException(error_messages)
|
|
285
|
+
|
|
286
|
+
@property
|
|
287
|
+
def success_response(self) -> SuccessResponse:
|
|
288
|
+
success = [msg for msg in self.data if isinstance(msg, SuccessResponse)]
|
|
289
|
+
if len(success) == 1:
|
|
290
|
+
return success[0]
|
|
291
|
+
elif success:
|
|
292
|
+
raise ValueError("Multiple successful HTTP responses found in the messages.")
|
|
293
|
+
else:
|
|
294
|
+
raise ValueError("No successful HTTP response found in the messages.")
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import threading
|
|
2
|
+
from dataclasses import dataclass, field
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
@dataclass
|
|
6
|
+
class ItemsRequestTracker:
|
|
7
|
+
"""Tracks the state of requests split from an original request.
|
|
8
|
+
|
|
9
|
+
Attributes:
|
|
10
|
+
max_failures_before_abort (int): Maximum number of allowed failed split requests before aborting
|
|
11
|
+
the entire operation. A value of -1 indicates no early abort.
|
|
12
|
+
lock (threading.Lock): A lock to ensure thread-safe updates to the failure count.
|
|
13
|
+
failed_split_count (int): The current count of failed split requests.
|
|
14
|
+
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
max_failures_before_abort: int = -1 # -1 means no early abort
|
|
18
|
+
lock: threading.Lock = field(default_factory=threading.Lock, init=False)
|
|
19
|
+
failed_split_count: int = field(default=0, init=False)
|
|
20
|
+
|
|
21
|
+
def register_failure(self) -> None:
|
|
22
|
+
"""Register a failed split request and return whether to continue splitting."""
|
|
23
|
+
with self.lock:
|
|
24
|
+
self.failed_split_count += 1
|
|
25
|
+
|
|
26
|
+
def limit_reached(self) -> bool:
|
|
27
|
+
"""Check if the failure limit has been reached."""
|
|
28
|
+
with self.lock:
|
|
29
|
+
if self.max_failures_before_abort < 0:
|
|
30
|
+
return False
|
|
31
|
+
return self.failed_split_count >= self.max_failures_before_abort
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import subprocess
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def get_repo_root() -> Path:
|
|
6
|
+
"""Get the root path of the git repository.
|
|
7
|
+
|
|
8
|
+
Raises:
|
|
9
|
+
RuntimeError: If git is not installed or the current directory is not in a git repository
|
|
10
|
+
|
|
11
|
+
"""
|
|
12
|
+
try:
|
|
13
|
+
result = subprocess.run("git rev-parse --show-toplevel".split(), stdout=subprocess.PIPE)
|
|
14
|
+
except FileNotFoundError as e:
|
|
15
|
+
raise RuntimeError("Git is not installed or not found in PATH") from e
|
|
16
|
+
output = result.stdout.decode().strip()
|
|
17
|
+
if not output:
|
|
18
|
+
raise RuntimeError("Not in a git repository")
|
|
19
|
+
return Path(output)
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from collections.abc import Collection
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
NEWLINE = "\n"
|
|
6
|
+
TAB = "\t"
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def humanize_collection(collection: Collection[Any], /, *, sort: bool = True, bind_word: str = "and") -> str:
|
|
10
|
+
"""Convert a collection of items to a human-readable string.
|
|
11
|
+
|
|
12
|
+
Args:
|
|
13
|
+
collection: The collection of items to convert.
|
|
14
|
+
sort: Whether to sort the collection before converting. Default is True.
|
|
15
|
+
bind_word: The word to use to bind the last two items. Default is "and".
|
|
16
|
+
|
|
17
|
+
Returns:
|
|
18
|
+
A human-readable string of the collection.
|
|
19
|
+
|
|
20
|
+
Examples:
|
|
21
|
+
>>> humanize_collection(["b", "c", "a"])
|
|
22
|
+
'a, b and c'
|
|
23
|
+
>>> humanize_collection(["b", "c", "a"], sort=False)
|
|
24
|
+
'b, c and a'
|
|
25
|
+
>>> humanize_collection(["a", "b"])
|
|
26
|
+
'a and b'
|
|
27
|
+
>>> humanize_collection(["a"])
|
|
28
|
+
'a'
|
|
29
|
+
>>> humanize_collection([])
|
|
30
|
+
''
|
|
31
|
+
|
|
32
|
+
"""
|
|
33
|
+
if not collection:
|
|
34
|
+
return ""
|
|
35
|
+
elif len(collection) == 1:
|
|
36
|
+
return str(next(iter(collection)))
|
|
37
|
+
|
|
38
|
+
strings = (str(item) for item in collection)
|
|
39
|
+
if sort:
|
|
40
|
+
sequence = sorted(strings)
|
|
41
|
+
else:
|
|
42
|
+
sequence = list(strings)
|
|
43
|
+
|
|
44
|
+
return f"{', '.join(sequence[:-1])} {bind_word} {sequence[-1]}"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def title_case(s: str) -> str:
|
|
48
|
+
"""Convert a string to title case, handling underscores and hyphens.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
s: The string to convert.
|
|
52
|
+
Returns:
|
|
53
|
+
The title-cased string.
|
|
54
|
+
Examples:
|
|
55
|
+
>>> title_case("hello world")
|
|
56
|
+
'Hello World'
|
|
57
|
+
>>> title_case("hello_world")
|
|
58
|
+
'Hello World'
|
|
59
|
+
>>> title_case("hello-world")
|
|
60
|
+
'Hello World'
|
|
61
|
+
>>> title_case("hello_world-and-universe")
|
|
62
|
+
'Hello World And Universe'
|
|
63
|
+
>>> title_case("HELLO WORLD")
|
|
64
|
+
'Hello World'
|
|
65
|
+
"""
|
|
66
|
+
return " ".join(word.capitalize() for word in s.replace("_", " ").replace("-", " ").split())
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def split_on_capitals(text: str) -> list[str]:
|
|
70
|
+
"""Split a string at capital letters."""
|
|
71
|
+
return re.findall(r"[A-Z][a-z]*", text)
|