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