dsp-tools 9.1.0.post11__py3-none-any.whl → 18.3.0.post13__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 (316) hide show
  1. dsp_tools/__init__.py +4 -0
  2. dsp_tools/cli/args.py +36 -0
  3. dsp_tools/cli/call_action.py +51 -231
  4. dsp_tools/cli/call_action_files_only.py +101 -0
  5. dsp_tools/cli/call_action_with_network.py +207 -0
  6. dsp_tools/cli/create_parsers.py +156 -58
  7. dsp_tools/cli/entry_point.py +56 -26
  8. dsp_tools/cli/utils.py +87 -0
  9. dsp_tools/clients/CLAUDE.md +420 -0
  10. dsp_tools/clients/authentication_client.py +14 -0
  11. dsp_tools/clients/authentication_client_live.py +66 -0
  12. dsp_tools/{utils → clients}/connection.py +2 -18
  13. dsp_tools/clients/connection_live.py +233 -0
  14. dsp_tools/clients/fuseki_metrics.py +60 -0
  15. dsp_tools/clients/group_user_clients.py +35 -0
  16. dsp_tools/clients/group_user_clients_live.py +181 -0
  17. dsp_tools/clients/legal_info_client.py +23 -0
  18. dsp_tools/clients/legal_info_client_live.py +132 -0
  19. dsp_tools/clients/list_client.py +49 -0
  20. dsp_tools/clients/list_client_live.py +166 -0
  21. dsp_tools/clients/metadata_client.py +24 -0
  22. dsp_tools/clients/metadata_client_live.py +47 -0
  23. dsp_tools/clients/ontology_clients.py +49 -0
  24. dsp_tools/clients/ontology_create_client_live.py +166 -0
  25. dsp_tools/clients/ontology_get_client_live.py +80 -0
  26. dsp_tools/clients/permissions_client.py +68 -0
  27. dsp_tools/clients/project_client.py +16 -0
  28. dsp_tools/clients/project_client_live.py +66 -0
  29. dsp_tools/commands/create/communicate_problems.py +24 -0
  30. dsp_tools/commands/create/create.py +134 -0
  31. dsp_tools/commands/create/create_on_server/cardinalities.py +111 -0
  32. dsp_tools/commands/create/create_on_server/classes.py +99 -0
  33. dsp_tools/commands/create/create_on_server/complete_ontologies.py +116 -0
  34. dsp_tools/commands/create/create_on_server/default_permissions.py +134 -0
  35. dsp_tools/commands/create/create_on_server/group_users.py +165 -0
  36. dsp_tools/commands/create/create_on_server/lists.py +163 -0
  37. dsp_tools/commands/create/create_on_server/mappers.py +12 -0
  38. dsp_tools/commands/create/create_on_server/onto_utils.py +74 -0
  39. dsp_tools/commands/create/create_on_server/ontology.py +52 -0
  40. dsp_tools/commands/create/create_on_server/project.py +68 -0
  41. dsp_tools/commands/create/create_on_server/properties.py +119 -0
  42. dsp_tools/commands/create/exceptions.py +29 -0
  43. dsp_tools/commands/create/lists_only.py +66 -0
  44. dsp_tools/commands/create/models/create_problems.py +87 -0
  45. dsp_tools/commands/create/models/parsed_ontology.py +88 -0
  46. dsp_tools/commands/create/models/parsed_project.py +81 -0
  47. dsp_tools/commands/create/models/rdf_ontology.py +12 -0
  48. dsp_tools/commands/create/models/server_project_info.py +100 -0
  49. dsp_tools/commands/create/parsing/parse_lists.py +45 -0
  50. dsp_tools/commands/create/parsing/parse_ontology.py +243 -0
  51. dsp_tools/commands/create/parsing/parse_project.py +149 -0
  52. dsp_tools/commands/create/parsing/parsing_utils.py +40 -0
  53. dsp_tools/commands/create/project_validate.py +595 -0
  54. dsp_tools/commands/create/serialisation/ontology.py +119 -0
  55. dsp_tools/commands/create/serialisation/project.py +44 -0
  56. dsp_tools/commands/excel2json/CLAUDE.md +101 -0
  57. dsp_tools/commands/excel2json/json_header.py +57 -23
  58. dsp_tools/commands/excel2json/{new_lists → lists}/compliance_checks.py +26 -26
  59. dsp_tools/commands/excel2json/{new_lists/make_new_lists.py → lists/make_lists.py} +19 -18
  60. dsp_tools/commands/excel2json/{new_lists → lists}/models/input_error.py +1 -12
  61. dsp_tools/commands/excel2json/{new_lists → lists}/models/serialise.py +9 -5
  62. dsp_tools/commands/excel2json/{new_lists → lists}/utils.py +4 -4
  63. dsp_tools/commands/excel2json/models/input_error.py +31 -11
  64. dsp_tools/commands/excel2json/models/json_header.py +53 -15
  65. dsp_tools/commands/excel2json/models/ontology.py +4 -3
  66. dsp_tools/commands/excel2json/{lists.py → old_lists.py} +26 -112
  67. dsp_tools/commands/excel2json/project.py +78 -34
  68. dsp_tools/commands/excel2json/properties.py +57 -36
  69. dsp_tools/commands/excel2json/resources.py +32 -12
  70. dsp_tools/commands/excel2json/utils.py +20 -1
  71. dsp_tools/commands/excel2xml/__init__.py +2 -2
  72. dsp_tools/commands/excel2xml/excel2xml_cli.py +7 -15
  73. dsp_tools/commands/excel2xml/excel2xml_lib.py +138 -493
  74. dsp_tools/commands/excel2xml/propertyelement.py +5 -5
  75. dsp_tools/commands/{project → get}/get.py +29 -13
  76. dsp_tools/commands/get/get_permissions.py +257 -0
  77. dsp_tools/commands/get/get_permissions_legacy.py +89 -0
  78. dsp_tools/commands/{project/models → get/legacy_models}/context.py +6 -6
  79. dsp_tools/commands/{project/models → get/legacy_models}/group.py +5 -10
  80. dsp_tools/commands/{project/models → get/legacy_models}/listnode.py +5 -35
  81. dsp_tools/commands/{project/models → get/legacy_models}/model.py +1 -1
  82. dsp_tools/commands/{project/models → get/legacy_models}/ontology.py +9 -14
  83. dsp_tools/commands/{project/models → get/legacy_models}/project.py +13 -6
  84. dsp_tools/commands/{project/models → get/legacy_models}/propertyclass.py +9 -16
  85. dsp_tools/commands/{project/models → get/legacy_models}/resourceclass.py +8 -46
  86. dsp_tools/commands/{project/models → get/legacy_models}/user.py +19 -60
  87. dsp_tools/commands/get/models/permissions_models.py +10 -0
  88. dsp_tools/commands/id2iri.py +20 -10
  89. dsp_tools/commands/ingest_xmlupload/bulk_ingest_client.py +81 -56
  90. dsp_tools/commands/ingest_xmlupload/create_resources/apply_ingest_id.py +4 -10
  91. dsp_tools/commands/ingest_xmlupload/create_resources/upload_xml.py +97 -37
  92. dsp_tools/commands/ingest_xmlupload/create_resources/user_information.py +2 -2
  93. dsp_tools/commands/ingest_xmlupload/ingest_files/ingest_files.py +9 -10
  94. dsp_tools/commands/ingest_xmlupload/upload_files/filechecker.py +3 -3
  95. dsp_tools/commands/ingest_xmlupload/upload_files/input_error.py +2 -10
  96. dsp_tools/commands/ingest_xmlupload/upload_files/upload_failures.py +12 -2
  97. dsp_tools/commands/ingest_xmlupload/upload_files/upload_files.py +8 -9
  98. dsp_tools/commands/resume_xmlupload/resume_xmlupload.py +18 -18
  99. dsp_tools/commands/start_stack.py +126 -77
  100. dsp_tools/commands/update_legal/CLAUDE.md +344 -0
  101. dsp_tools/commands/update_legal/__init__.py +0 -0
  102. dsp_tools/commands/update_legal/core.py +182 -0
  103. dsp_tools/commands/update_legal/csv_operations.py +135 -0
  104. dsp_tools/commands/update_legal/models.py +87 -0
  105. dsp_tools/commands/update_legal/xml_operations.py +247 -0
  106. dsp_tools/commands/validate_data/CLAUDE.md +159 -0
  107. dsp_tools/commands/validate_data/__init__.py +0 -0
  108. dsp_tools/commands/validate_data/constants.py +59 -0
  109. dsp_tools/commands/validate_data/mappers.py +143 -0
  110. dsp_tools/commands/validate_data/models/__init__.py +0 -0
  111. dsp_tools/commands/validate_data/models/api_responses.py +45 -0
  112. dsp_tools/commands/validate_data/models/input_problems.py +119 -0
  113. dsp_tools/commands/validate_data/models/rdf_like_data.py +117 -0
  114. dsp_tools/commands/validate_data/models/validation.py +106 -0
  115. dsp_tools/commands/validate_data/prepare_data/__init__.py +0 -0
  116. dsp_tools/commands/validate_data/prepare_data/get_rdf_like_data.py +296 -0
  117. dsp_tools/commands/validate_data/prepare_data/make_data_graph.py +91 -0
  118. dsp_tools/commands/validate_data/prepare_data/prepare_data.py +184 -0
  119. dsp_tools/commands/validate_data/process_validation_report/__init__.py +0 -0
  120. dsp_tools/commands/validate_data/process_validation_report/get_user_validation_message.py +358 -0
  121. dsp_tools/commands/validate_data/process_validation_report/query_validation_result.py +507 -0
  122. dsp_tools/commands/validate_data/process_validation_report/reformat_validation_results.py +150 -0
  123. dsp_tools/commands/validate_data/shacl_cli_validator.py +70 -0
  124. dsp_tools/commands/validate_data/sparql/__init__.py +0 -0
  125. dsp_tools/commands/{xml_validate/sparql/resource_shacl.py → validate_data/sparql/cardinality_shacl.py} +45 -47
  126. dsp_tools/commands/validate_data/sparql/construct_shacl.py +92 -0
  127. dsp_tools/commands/validate_data/sparql/legal_info_shacl.py +36 -0
  128. dsp_tools/commands/validate_data/sparql/value_shacl.py +357 -0
  129. dsp_tools/commands/validate_data/utils.py +59 -0
  130. dsp_tools/commands/validate_data/validate_data.py +283 -0
  131. dsp_tools/commands/validate_data/validation/__init__.py +0 -0
  132. dsp_tools/commands/validate_data/validation/check_duplicate_files.py +55 -0
  133. dsp_tools/commands/validate_data/validation/check_for_unknown_classes.py +67 -0
  134. dsp_tools/commands/validate_data/validation/get_validation_report.py +94 -0
  135. dsp_tools/commands/validate_data/validation/validate_ontology.py +107 -0
  136. dsp_tools/commands/xmlupload/CLAUDE.md +292 -0
  137. dsp_tools/commands/xmlupload/make_rdf_graph/__init__.py +0 -0
  138. dsp_tools/commands/xmlupload/make_rdf_graph/constants.py +63 -0
  139. dsp_tools/commands/xmlupload/make_rdf_graph/jsonld_utils.py +44 -0
  140. dsp_tools/commands/xmlupload/make_rdf_graph/make_file_value.py +77 -0
  141. dsp_tools/commands/xmlupload/make_rdf_graph/make_resource_and_values.py +114 -0
  142. dsp_tools/commands/xmlupload/make_rdf_graph/make_values.py +262 -0
  143. dsp_tools/commands/xmlupload/models/bitstream_info.py +18 -0
  144. dsp_tools/commands/xmlupload/models/formatted_text_value.py +0 -25
  145. dsp_tools/commands/xmlupload/models/ingest.py +56 -70
  146. dsp_tools/commands/xmlupload/models/input_problems.py +6 -14
  147. dsp_tools/commands/xmlupload/models/lookup_models.py +21 -0
  148. dsp_tools/commands/xmlupload/models/permission.py +0 -39
  149. dsp_tools/commands/xmlupload/models/{deserialise/xmlpermission.py → permissions_parsed.py} +2 -2
  150. dsp_tools/commands/xmlupload/models/processed/__init__.py +0 -0
  151. dsp_tools/commands/xmlupload/models/processed/file_values.py +29 -0
  152. dsp_tools/commands/xmlupload/models/processed/res.py +27 -0
  153. dsp_tools/commands/xmlupload/models/processed/values.py +101 -0
  154. dsp_tools/commands/xmlupload/models/rdf_models.py +26 -0
  155. dsp_tools/commands/xmlupload/models/upload_clients.py +3 -3
  156. dsp_tools/commands/xmlupload/models/upload_state.py +2 -4
  157. dsp_tools/commands/xmlupload/prepare_xml_input/__init__.py +0 -0
  158. dsp_tools/commands/xmlupload/{ark2iri.py → prepare_xml_input/ark2iri.py} +1 -1
  159. dsp_tools/commands/xmlupload/prepare_xml_input/get_processed_resources.py +252 -0
  160. dsp_tools/commands/xmlupload/{iiif_uri_validator.py → prepare_xml_input/iiif_uri_validator.py} +2 -14
  161. dsp_tools/commands/xmlupload/{list_client.py → prepare_xml_input/list_client.py} +15 -10
  162. dsp_tools/commands/xmlupload/prepare_xml_input/prepare_xml_input.py +67 -0
  163. dsp_tools/commands/xmlupload/prepare_xml_input/read_validate_xml_file.py +58 -0
  164. dsp_tools/commands/xmlupload/prepare_xml_input/transform_input_values.py +118 -0
  165. dsp_tools/commands/xmlupload/resource_create_client.py +7 -468
  166. dsp_tools/commands/xmlupload/richtext_id2iri.py +37 -0
  167. dsp_tools/commands/xmlupload/stash/{construct_and_analyze_graph.py → analyse_circular_reference_graph.py} +64 -157
  168. dsp_tools/commands/xmlupload/stash/create_info_for_graph.py +53 -0
  169. dsp_tools/commands/xmlupload/stash/graph_models.py +13 -8
  170. dsp_tools/commands/xmlupload/stash/stash_circular_references.py +48 -115
  171. dsp_tools/commands/xmlupload/stash/stash_models.py +4 -9
  172. dsp_tools/commands/xmlupload/stash/upload_stashed_resptr_props.py +34 -40
  173. dsp_tools/commands/xmlupload/stash/upload_stashed_xml_texts.py +98 -108
  174. dsp_tools/commands/xmlupload/upload_config.py +8 -0
  175. dsp_tools/commands/xmlupload/write_diagnostic_info.py +14 -9
  176. dsp_tools/commands/xmlupload/xmlupload.py +214 -192
  177. dsp_tools/config/__init__.py +0 -0
  178. dsp_tools/config/logger_config.py +69 -0
  179. dsp_tools/{utils → config}/warnings_config.py +4 -1
  180. dsp_tools/error/__init__.py +0 -0
  181. dsp_tools/error/custom_warnings.py +39 -0
  182. dsp_tools/error/exceptions.py +204 -0
  183. dsp_tools/error/problems.py +10 -0
  184. dsp_tools/error/xmllib_errors.py +20 -0
  185. dsp_tools/error/xmllib_warnings.py +54 -0
  186. dsp_tools/error/xmllib_warnings_util.py +159 -0
  187. dsp_tools/error/xsd_validation_error_msg.py +19 -0
  188. dsp_tools/legacy_models/__init__.py +0 -0
  189. dsp_tools/{models → legacy_models}/datetimestamp.py +7 -7
  190. dsp_tools/{models → legacy_models}/langstring.py +1 -1
  191. dsp_tools/{models → legacy_models}/projectContext.py +4 -4
  192. dsp_tools/resources/schema/data.xsd +108 -83
  193. dsp_tools/resources/schema/lists-only.json +4 -23
  194. dsp_tools/resources/schema/project.json +80 -35
  195. dsp_tools/resources/schema/properties-only.json +1 -4
  196. dsp_tools/resources/start-stack/docker-compose.override-host.j2 +11 -0
  197. dsp_tools/resources/start-stack/docker-compose.yml +34 -30
  198. dsp_tools/resources/start-stack/dsp-app-config.json +45 -0
  199. dsp_tools/resources/start-stack/dsp-app-config.override-host.j2 +26 -0
  200. dsp_tools/resources/validate_data/api-shapes-resource-cardinalities.ttl +191 -0
  201. dsp_tools/resources/validate_data/api-shapes.ttl +804 -0
  202. dsp_tools/resources/validate_data/shacl-cli-image.yml +4 -0
  203. dsp_tools/resources/validate_data/validate-ontology.ttl +99 -0
  204. dsp_tools/utils/ansi_colors.py +32 -0
  205. dsp_tools/utils/data_formats/__init__.py +0 -0
  206. dsp_tools/utils/{date_util.py → data_formats/date_util.py} +13 -1
  207. dsp_tools/utils/data_formats/iri_util.py +30 -0
  208. dsp_tools/utils/{shared.py → data_formats/shared.py} +1 -35
  209. dsp_tools/utils/{uri_util.py → data_formats/uri_util.py} +12 -2
  210. dsp_tools/utils/fuseki_bloating.py +63 -0
  211. dsp_tools/utils/json_parsing.py +22 -0
  212. dsp_tools/utils/rdf_constants.py +42 -0
  213. dsp_tools/utils/rdflib_utils.py +10 -0
  214. dsp_tools/utils/replace_id_with_iri.py +66 -0
  215. dsp_tools/utils/request_utils.py +238 -0
  216. dsp_tools/utils/xml_parsing/__init__.py +0 -0
  217. dsp_tools/utils/xml_parsing/get_lookups.py +32 -0
  218. dsp_tools/utils/xml_parsing/get_parsed_resources.py +325 -0
  219. dsp_tools/utils/xml_parsing/models/__init__.py +0 -0
  220. dsp_tools/utils/xml_parsing/models/parsed_resource.py +76 -0
  221. dsp_tools/utils/xml_parsing/parse_clean_validate_xml.py +137 -0
  222. dsp_tools/xmllib/CLAUDE.md +302 -0
  223. dsp_tools/xmllib/__init__.py +49 -0
  224. dsp_tools/xmllib/general_functions.py +877 -0
  225. dsp_tools/xmllib/internal/__init__.py +0 -0
  226. dsp_tools/xmllib/internal/checkers.py +162 -0
  227. dsp_tools/xmllib/internal/circumvent_circular_imports.py +36 -0
  228. dsp_tools/xmllib/internal/constants.py +46 -0
  229. dsp_tools/xmllib/internal/input_converters.py +155 -0
  230. dsp_tools/xmllib/internal/serialise_file_value.py +57 -0
  231. dsp_tools/xmllib/internal/serialise_resource.py +177 -0
  232. dsp_tools/xmllib/internal/serialise_values.py +152 -0
  233. dsp_tools/xmllib/internal/type_aliases.py +11 -0
  234. dsp_tools/xmllib/models/config_options.py +28 -0
  235. dsp_tools/xmllib/models/date_formats.py +48 -0
  236. dsp_tools/xmllib/models/dsp_base_resources.py +1380 -400
  237. dsp_tools/xmllib/models/internal/__init__.py +0 -0
  238. dsp_tools/xmllib/models/internal/file_values.py +172 -0
  239. dsp_tools/xmllib/models/internal/geometry.py +162 -0
  240. dsp_tools/xmllib/models/{migration_metadata.py → internal/migration_metadata.py} +14 -10
  241. dsp_tools/xmllib/models/internal/serialise_permissions.py +66 -0
  242. dsp_tools/xmllib/models/internal/values.py +342 -0
  243. dsp_tools/xmllib/models/licenses/__init__.py +0 -0
  244. dsp_tools/xmllib/models/licenses/other.py +59 -0
  245. dsp_tools/xmllib/models/licenses/recommended.py +107 -0
  246. dsp_tools/xmllib/models/permissions.py +41 -0
  247. dsp_tools/xmllib/models/res.py +1782 -0
  248. dsp_tools/xmllib/models/root.py +313 -26
  249. dsp_tools/xmllib/value_checkers.py +310 -47
  250. dsp_tools/xmllib/value_converters.py +765 -8
  251. dsp_tools-18.3.0.post13.dist-info/METADATA +90 -0
  252. dsp_tools-18.3.0.post13.dist-info/RECORD +286 -0
  253. dsp_tools-18.3.0.post13.dist-info/WHEEL +4 -0
  254. {dsp_tools-9.1.0.post11.dist-info → dsp_tools-18.3.0.post13.dist-info}/entry_points.txt +1 -0
  255. dsp_tools/commands/project/create/project_create.py +0 -1107
  256. dsp_tools/commands/project/create/project_create_lists.py +0 -204
  257. dsp_tools/commands/project/create/project_validate.py +0 -453
  258. dsp_tools/commands/project/models/project_definition.py +0 -12
  259. dsp_tools/commands/rosetta.py +0 -124
  260. dsp_tools/commands/template.py +0 -30
  261. dsp_tools/commands/xml_validate/api_connection.py +0 -122
  262. dsp_tools/commands/xml_validate/deserialise_input.py +0 -135
  263. dsp_tools/commands/xml_validate/make_data_rdf.py +0 -193
  264. dsp_tools/commands/xml_validate/models/data_deserialised.py +0 -108
  265. dsp_tools/commands/xml_validate/models/data_rdf.py +0 -214
  266. dsp_tools/commands/xml_validate/models/input_problems.py +0 -191
  267. dsp_tools/commands/xml_validate/models/validation.py +0 -29
  268. dsp_tools/commands/xml_validate/reformat_validaton_result.py +0 -89
  269. dsp_tools/commands/xml_validate/sparql/construct_shapes.py +0 -16
  270. dsp_tools/commands/xml_validate/xml_validate.py +0 -151
  271. dsp_tools/commands/xmlupload/check_consistency_with_ontology.py +0 -253
  272. dsp_tools/commands/xmlupload/models/deserialise/deserialise_value.py +0 -236
  273. dsp_tools/commands/xmlupload/models/deserialise/xmlresource.py +0 -171
  274. dsp_tools/commands/xmlupload/models/namespace_context.py +0 -39
  275. dsp_tools/commands/xmlupload/models/ontology_lookup_models.py +0 -161
  276. dsp_tools/commands/xmlupload/models/ontology_problem_models.py +0 -178
  277. dsp_tools/commands/xmlupload/models/serialise/jsonld_serialiser.py +0 -40
  278. dsp_tools/commands/xmlupload/models/serialise/serialise_value.py +0 -51
  279. dsp_tools/commands/xmlupload/ontology_client.py +0 -92
  280. dsp_tools/commands/xmlupload/project_client.py +0 -91
  281. dsp_tools/commands/xmlupload/read_validate_xml_file.py +0 -99
  282. dsp_tools/models/custom_warnings.py +0 -31
  283. dsp_tools/models/exceptions.py +0 -90
  284. dsp_tools/resources/0100-template-repo/template.json +0 -45
  285. dsp_tools/resources/0100-template-repo/template.xml +0 -27
  286. dsp_tools/resources/start-stack/docker-compose-validation.yml +0 -5
  287. dsp_tools/resources/start-stack/start-stack-config.yml +0 -4
  288. dsp_tools/resources/xml_validate/api-shapes.ttl +0 -411
  289. dsp_tools/resources/xml_validate/replace_namespace.xslt +0 -61
  290. dsp_tools/utils/connection_live.py +0 -383
  291. dsp_tools/utils/iri_util.py +0 -14
  292. dsp_tools/utils/logger_config.py +0 -41
  293. dsp_tools/utils/set_encoder.py +0 -20
  294. dsp_tools/utils/xml_utils.py +0 -145
  295. dsp_tools/utils/xml_validation.py +0 -197
  296. dsp_tools/utils/xml_validation_models.py +0 -68
  297. dsp_tools/xmllib/models/file_values.py +0 -78
  298. dsp_tools/xmllib/models/resource.py +0 -415
  299. dsp_tools/xmllib/models/values.py +0 -428
  300. dsp_tools-9.1.0.post11.dist-info/METADATA +0 -130
  301. dsp_tools-9.1.0.post11.dist-info/RECORD +0 -167
  302. dsp_tools-9.1.0.post11.dist-info/WHEEL +0 -4
  303. dsp_tools-9.1.0.post11.dist-info/licenses/LICENSE +0 -674
  304. /dsp_tools/{commands/excel2json/new_lists → clients}/__init__.py +0 -0
  305. /dsp_tools/commands/{excel2json/new_lists/models → create}/__init__.py +0 -0
  306. /dsp_tools/commands/{project → create/create_on_server}/__init__.py +0 -0
  307. /dsp_tools/commands/{project/create → create/models}/__init__.py +0 -0
  308. /dsp_tools/commands/{project/models → create/parsing}/__init__.py +0 -0
  309. /dsp_tools/commands/{xml_validate → create/serialisation}/__init__.py +0 -0
  310. /dsp_tools/commands/{xml_validate/models → excel2json/lists}/__init__.py +0 -0
  311. /dsp_tools/commands/{xml_validate/sparql → excel2json/lists/models}/__init__.py +0 -0
  312. /dsp_tools/commands/excel2json/{new_lists → lists}/models/deserialise.py +0 -0
  313. /dsp_tools/commands/{xmlupload/models/deserialise → get}/__init__.py +0 -0
  314. /dsp_tools/commands/{xmlupload/models/serialise → get/legacy_models}/__init__.py +0 -0
  315. /dsp_tools/commands/{project/models → get/legacy_models}/helpers.py +0 -0
  316. /dsp_tools/{models → commands/get/models}/__init__.py +0 -0
@@ -0,0 +1,595 @@
1
+ from __future__ import annotations
2
+
3
+ import importlib.resources
4
+ import json
5
+ from pathlib import Path
6
+ from typing import Any
7
+
8
+ import jsonpath_ng
9
+ import jsonpath_ng.ext
10
+ import jsonschema
11
+ import networkx as nx
12
+ import regex
13
+ from loguru import logger
14
+
15
+ from dsp_tools.commands.create.communicate_problems import print_all_problem_collections
16
+ from dsp_tools.commands.create.exceptions import ProjectJsonSchemaValidationError
17
+ from dsp_tools.commands.create.models.create_problems import CollectedProblems
18
+ from dsp_tools.commands.create.models.create_problems import CreateProblem
19
+ from dsp_tools.commands.create.models.create_problems import InputProblem
20
+ from dsp_tools.commands.create.models.create_problems import InputProblemType
21
+ from dsp_tools.commands.create.models.parsed_project import ParsedProject
22
+ from dsp_tools.commands.create.parsing.parse_project import parse_project
23
+ from dsp_tools.utils.ansi_colors import BACKGROUND_BOLD_GREEN
24
+ from dsp_tools.utils.ansi_colors import RESET_TO_DEFAULT
25
+ from dsp_tools.utils.json_parsing import parse_json_file
26
+
27
+
28
+ def validate_project_only(project_file: Path, server: str) -> bool:
29
+ result = parse_and_validate_project(project_file, server)
30
+ if not isinstance(result, ParsedProject):
31
+ print_all_problem_collections(result)
32
+ return False
33
+ print(
34
+ BACKGROUND_BOLD_GREEN + "JSON project file is syntactically correct and passed validation." + RESET_TO_DEFAULT
35
+ )
36
+ return True
37
+
38
+
39
+ def parse_and_validate_project(project_file: Path, server: str) -> list[CollectedProblems] | ParsedProject:
40
+ json_project = parse_json_file(project_file)
41
+ return _validate_parsed_json_project(json_project, server)
42
+
43
+
44
+ def _validate_parsed_json_project(json_project: dict[str, Any], server: str) -> list[CollectedProblems] | ParsedProject:
45
+ _validate_with_json_schema(json_project)
46
+ parsing_result = parse_project(json_project, server)
47
+ if not isinstance(parsing_result, ParsedProject):
48
+ return parsing_result
49
+ if json_validation_problems := _complex_json_project_validation(json_project):
50
+ return json_validation_problems
51
+ return parsing_result
52
+
53
+
54
+ def _validate_with_json_schema(project_definition: dict[str, Any]) -> None:
55
+ with (
56
+ importlib.resources.files("dsp_tools")
57
+ .joinpath("resources/schema/project.json")
58
+ .open(encoding="utf-8") as schema_file
59
+ ):
60
+ project_schema = json.load(schema_file)
61
+ try:
62
+ jsonschema.validate(instance=project_definition, schema=project_schema)
63
+ except jsonschema.ValidationError as err:
64
+ logger.error(err)
65
+ # Check for the specific case of missing 'default_permissions'
66
+ if "'default_permissions' is a required property" in err.message:
67
+ raise ProjectJsonSchemaValidationError("You forgot to specify the 'default_permissions'") from None
68
+ # Check for the specific case of private permissions with overrule
69
+ if (
70
+ "should not be valid under {'required': ['default_permissions_overrule']}" in err.message
71
+ and project_definition.get("project", {}).get("default_permissions") == "private"
72
+ ):
73
+ raise ProjectJsonSchemaValidationError(
74
+ "When default_permissions is 'private', default_permissions_overrule cannot be specified. "
75
+ "Private permissions cannot be overruled."
76
+ ) from None
77
+
78
+ raise ProjectJsonSchemaValidationError(
79
+ f"The JSON project file cannot be created due to the following validation error: {err.message}.\n"
80
+ f"The error occurred at {err.json_path}:\n{err.instance}"
81
+ ) from None
82
+
83
+
84
+ def _complex_json_project_validation(project_definition: dict[str, Any]) -> list[CollectedProblems]:
85
+ problems: list[CollectedProblems] = []
86
+ # make some checks that are too complex for JSON schema
87
+ if perm_res := _check_for_invalid_default_permissions_overrule(project_definition):
88
+ problems.append(perm_res)
89
+ if prop_problem := _check_for_undefined_super_property(project_definition):
90
+ problems.append(prop_problem)
91
+ if cls_problems := _check_for_undefined_super_class(project_definition):
92
+ problems.append(cls_problems)
93
+ if card_problem := _check_for_undefined_cardinalities(project_definition):
94
+ problems.append(card_problem)
95
+ duplicated = _check_for_duplicate_res_and_props(project_definition)
96
+ problems.extend(duplicated)
97
+ if lists_section := project_definition["project"].get("lists"):
98
+ if list_prob := _check_for_duplicate_listnodes(lists_section):
99
+ problems.append(list_prob)
100
+ if card_probs := _check_cardinalities_of_circular_references(project_definition):
101
+ problems.append(card_probs)
102
+ return problems
103
+
104
+
105
+ def _build_resource_lookup(project_definition: dict[str, Any]) -> dict[str, dict[str, dict[str, Any]]]:
106
+ resource_lookup: dict[str, dict[str, dict[str, Any]]] = {}
107
+ for onto in project_definition["project"]["ontologies"]:
108
+ resource_lookup[onto["name"]] = {}
109
+ for resource in onto["resources"]:
110
+ resource_lookup[onto["name"]][resource["name"]] = resource
111
+ return resource_lookup
112
+
113
+
114
+ def _parse_class_reference(class_ref: str) -> tuple[str, str] | None:
115
+ """
116
+ Parse a class reference in the format 'ontology:ClassName'.
117
+
118
+ Args:
119
+ class_ref: Class reference string
120
+
121
+ Returns:
122
+ Tuple of (ontology_name, class_name) or None if invalid format
123
+ """
124
+ if ":" not in class_ref:
125
+ return None
126
+
127
+ parts = class_ref.split(":")
128
+ if len(parts) != 2:
129
+ return None
130
+
131
+ return parts[0], parts[1]
132
+
133
+
134
+ def _is_subclass_of_still_image_representation(
135
+ ontology_name: str, class_name: str, resource_lookup: dict[str, dict[str, dict[str, Any]]]
136
+ ) -> bool:
137
+ """
138
+ Check if a class is a subclass of StillImageRepresentation by traversing the inheritance chain.
139
+
140
+ Args:
141
+ ontology_name: Name of the ontology containing the class
142
+ class_name: Name of the class to check
143
+ resource_lookup: Dictionary mapping ontology names to resource definitions
144
+
145
+ Returns:
146
+ True if the class is a subclass of StillImageRepresentation
147
+ """
148
+ current_onto = ontology_name
149
+ current_class = class_name
150
+ visited = set()
151
+
152
+ # Follow the inheritance chain up to 10 levels (prevent infinite loops)
153
+ for _ in range(10):
154
+ class_id = f"{current_onto}:{current_class}"
155
+ if class_id in visited:
156
+ break # Circular reference detected
157
+ visited.add(class_id)
158
+
159
+ if current_onto not in resource_lookup or current_class not in resource_lookup[current_onto]:
160
+ break # Resource not found
161
+
162
+ resource = resource_lookup[current_onto][current_class]
163
+ super_class = resource.get("super")
164
+
165
+ # Handle both string and list formats for super
166
+ super_classes = []
167
+ if isinstance(super_class, list):
168
+ super_classes = super_class
169
+ elif super_class:
170
+ super_classes = [super_class]
171
+
172
+ # Check if any superclass is StillImageRepresentation
173
+ if "StillImageRepresentation" in super_classes:
174
+ return True
175
+
176
+ # Find the next class in the inheritance chain
177
+ next_class = None
178
+ for super_cls in super_classes:
179
+ if super_cls.startswith(":"):
180
+ # Same ontology reference
181
+ next_class = (current_onto, super_cls[1:])
182
+ break
183
+ elif ":" in super_cls and super_cls != "StillImageRepresentation":
184
+ # Different ontology reference
185
+ super_parts = super_cls.split(":", 1)
186
+ if len(super_parts) == 2:
187
+ next_class = (super_parts[0], super_parts[1])
188
+ break
189
+
190
+ if next_class:
191
+ current_onto, current_class = next_class
192
+ else:
193
+ break # No more inheritance to follow
194
+
195
+ return False
196
+
197
+
198
+ def _check_for_invalid_default_permissions_overrule(project_definition: dict[str, Any]) -> CollectedProblems | None:
199
+ if not (default_permissions_overrule := project_definition.get("project", {}).get("default_permissions_overrule")):
200
+ return None
201
+ if not (limited_view := default_permissions_overrule.get("limited_view")):
202
+ return None
203
+
204
+ # If limited_view is "all", no validation needed - it applies to all image classes
205
+ if limited_view == "all":
206
+ return None
207
+
208
+ problems: list[CreateProblem] = []
209
+ resource_lookup = _build_resource_lookup(project_definition)
210
+
211
+ # Check each class in limited_view (when it's a list)
212
+ for class_ref in limited_view:
213
+ parsed_ref = _parse_class_reference(class_ref)
214
+ if not parsed_ref:
215
+ problems.append(
216
+ InputProblem(
217
+ problematic_object=f"{class_ref} (Invalid format, expected 'ontology:ClassName')",
218
+ problem=InputProblemType.PREFIX_COULD_NOT_BE_RESOLVED,
219
+ )
220
+ )
221
+ continue
222
+
223
+ ontology_name, class_name = parsed_ref
224
+
225
+ # Check if the ontology exists
226
+ if ontology_name not in resource_lookup:
227
+ problems.append(
228
+ InputProblem(
229
+ problematic_object=f"{ontology_name}:{class_name} (Ontology '{ontology_name}' not found)",
230
+ problem=InputProblemType.PREFIX_COULD_NOT_BE_RESOLVED,
231
+ )
232
+ )
233
+ continue
234
+
235
+ # Check if the resource exists in the ontology
236
+ if class_name not in resource_lookup[ontology_name]:
237
+ problems.append(
238
+ InputProblem(
239
+ problematic_object=(
240
+ f"{ontology_name}:{class_name} "
241
+ f"(Resource '{class_name}' not found in ontology '{ontology_name}')"
242
+ ),
243
+ problem=InputProblemType.UNDEFINED_REFERENCE,
244
+ )
245
+ )
246
+ continue
247
+
248
+ # Check if the resource is a subclass of StillImageRepresentation
249
+ if not _is_subclass_of_still_image_representation(ontology_name, class_name, resource_lookup):
250
+ problems.append(
251
+ InputProblem(
252
+ problematic_object=(
253
+ f"{ontology_name}:{class_name} "
254
+ f"(Must be a subclass of 'StillImageRepresentation' directly or through inheritance)"
255
+ ),
256
+ problem=InputProblemType.INVALID_PERMISSIONS_OVERRULE,
257
+ )
258
+ )
259
+ if problems:
260
+ err_msg = (
261
+ "All classes in project.default_permissions_overrule.limited_view "
262
+ "must be subclasses of 'StillImageRepresentation', because the 'limited view' "
263
+ "permission is only implemented for images (i.e. blurring, watermarking). \n"
264
+ "In order to check, the classes must be provided in the form \n"
265
+ ' "limited_view": ["ontoname:Classname", ...]\n\n'
266
+ "The following classes do not meet this requirement:\n"
267
+ )
268
+ return CollectedProblems(err_msg, problems)
269
+ return None
270
+
271
+
272
+ def _check_for_undefined_super_property(project_definition: dict[str, Any]) -> CollectedProblems | None:
273
+ problems: list[CreateProblem] = []
274
+ for onto in project_definition["project"]["ontologies"]:
275
+ ontoname = onto["name"]
276
+ propnames = [p["name"] for p in onto["properties"]]
277
+ for prop in onto["properties"]:
278
+ supers = prop["super"]
279
+ # form of supers:
280
+ # - isSegmentOf # DSP base property
281
+ # - other:prop # other onto
282
+ # - same:prop # same onto
283
+ # - :prop # same onto (short form)
284
+
285
+ # filter out DSP base properties
286
+ supers = [s for s in supers if ":" in s]
287
+ # extend short form
288
+ supers = [regex.sub(r"^:", f"{ontoname}:", s) for s in supers]
289
+ # filter out other ontos
290
+ supers = [s for s in supers if regex.search(f"^{ontoname}:", s)]
291
+ # convert to short form
292
+ supers = [regex.sub(f"^{ontoname}", "", s) for s in supers]
293
+
294
+ if invalid_references := [s for s in supers if regex.sub(":", "", s) not in propnames]:
295
+ invalid_refs_str = ", ".join(invalid_references)
296
+ problems.append(
297
+ InputProblem(
298
+ problematic_object=f"{ontoname}:{prop['name']} (invalid super-properties: {invalid_refs_str})",
299
+ problem=InputProblemType.UNDEFINED_SUPER_PROPERTY,
300
+ )
301
+ )
302
+ if problems:
303
+ return CollectedProblems("The following properties have undefined super-properties:", problems)
304
+ return None
305
+
306
+
307
+ def _find_duplicate_res_and_props(
308
+ project_definition: dict[str, Any],
309
+ ) -> tuple[dict[str, set[str]], dict[str, set[str]]]:
310
+ resnames_duplicates: dict[str, set[str]] = {}
311
+ propnames_duplicates: dict[str, set[str]] = {}
312
+ for onto in project_definition["project"]["ontologies"]:
313
+ resnames = [r["name"] for r in onto["resources"]]
314
+ if len(set(resnames)) != len(resnames):
315
+ for elem in resnames:
316
+ if resnames.count(elem) > 1:
317
+ if resnames_duplicates.get(onto["name"]):
318
+ resnames_duplicates[onto["name"]].add(elem)
319
+ else:
320
+ resnames_duplicates[onto["name"]] = {elem}
321
+
322
+ propnames = [p["name"] for p in onto["properties"]]
323
+ if len(set(propnames)) != len(propnames):
324
+ for elem in propnames:
325
+ if propnames.count(elem) > 1:
326
+ if propnames_duplicates.get(onto["name"]):
327
+ propnames_duplicates[onto["name"]].add(elem)
328
+ else:
329
+ propnames_duplicates[onto["name"]] = {elem}
330
+ return propnames_duplicates, resnames_duplicates
331
+
332
+
333
+ def _find_duplicate_listnodes(lists_section: list[dict[str, Any]]) -> set[str]:
334
+ def _process_sublist(sublist: dict[str, Any]) -> None:
335
+ existing_nodenames.append(sublist["name"])
336
+ if nodes := sublist.get("nodes"):
337
+ if isinstance(nodes, dict) and list(nodes.keys()) == ["folder"]:
338
+ return
339
+ for x in nodes:
340
+ _process_sublist(x)
341
+
342
+ existing_nodenames: list[str] = []
343
+ for lst in lists_section:
344
+ _process_sublist(lst)
345
+
346
+ return {x for x in existing_nodenames if existing_nodenames.count(x) > 1}
347
+
348
+
349
+ def _check_for_undefined_super_class(project_definition: dict[str, Any]) -> CollectedProblems | None:
350
+ problems: list[CreateProblem] = []
351
+ for onto in project_definition["project"]["ontologies"]:
352
+ ontoname = onto["name"]
353
+ resnames = [r["name"] for r in onto["resources"]]
354
+ for res in onto["resources"]:
355
+ supers = res["super"] if isinstance(res["super"], list) else [res["super"]]
356
+ # form of supers:
357
+ # - Resource # DSP base resource
358
+ # - other:res # other onto
359
+ # - same:res # same onto
360
+ # - :res # same onto (short form)
361
+
362
+ # filter out DSP base resources
363
+ supers = [s for s in supers if ":" in s]
364
+ # extend short form
365
+ supers = [regex.sub(r"^:", f"{ontoname}:", s) for s in supers]
366
+ # filter out other ontos
367
+ supers = [s for s in supers if regex.search(f"^{ontoname}:", s)]
368
+ # convert to short form
369
+ supers = [regex.sub(f"^{ontoname}", "", s) for s in supers]
370
+
371
+ if invalid_references := [s for s in supers if regex.sub(":", "", s) not in resnames]:
372
+ invalid_refs_str = ", ".join(invalid_references)
373
+ problems.append(
374
+ InputProblem(
375
+ problematic_object=f"{ontoname}:{res['name']} (invalid super-classes: {invalid_refs_str})",
376
+ problem=InputProblemType.UNDEFINED_SUPER_CLASS,
377
+ )
378
+ )
379
+ if problems:
380
+ return CollectedProblems("The following classes have undefined super-classes:", problems)
381
+ return None
382
+
383
+
384
+ def _check_for_undefined_cardinalities(project_definition: dict[str, Any]) -> CollectedProblems | None:
385
+ problems: list[CreateProblem] = []
386
+ for onto in project_definition["project"]["ontologies"]:
387
+ ontoname = onto["name"]
388
+ propnames = [prop["name"] for prop in onto["properties"]]
389
+ for res in onto["resources"]:
390
+ cardnames = [card["propname"] for card in res.get("cardinalities", [])]
391
+ # form of the cardnames:
392
+ # - isSegmentOf # DSP base property
393
+ # - other:prop # other onto
394
+ # - same:prop # same onto
395
+ # - :prop # same onto (short form)
396
+
397
+ # filter out DSP base properties
398
+ cardnames = [card for card in cardnames if ":" in card]
399
+ # extend short form
400
+ cardnames = [regex.sub(r"^:", f"{ontoname}:", card) for card in cardnames]
401
+ # filter out other ontos
402
+ cardnames = [card for card in cardnames if regex.search(f"^{ontoname}:", card)]
403
+ # convert to short form
404
+ cardnames = [regex.sub(f"^{ontoname}:", ":", card) for card in cardnames]
405
+
406
+ if invalid_cardnames := [card for card in cardnames if regex.sub(":", "", card) not in propnames]:
407
+ invalid_cards_str = ", ".join(invalid_cardnames)
408
+ problems.append(
409
+ InputProblem(
410
+ problematic_object=f"{ontoname}:{res['name']} (invalid cardinalities: {invalid_cards_str})",
411
+ problem=InputProblemType.UNDEFINED_PROPERTY_IN_CARDINALITY,
412
+ )
413
+ )
414
+ if problems:
415
+ return CollectedProblems("The following classes have cardinalities for properties that do not exist:", problems)
416
+ return None
417
+
418
+
419
+ def _check_cardinalities_of_circular_references(project_definition: dict[Any, Any]) -> CollectedProblems | None:
420
+ link_properties = _collect_link_properties(project_definition)
421
+ errors = _identify_problematic_cardinalities(project_definition, link_properties)
422
+ if errors:
423
+ msg = (
424
+ "ERROR: Your ontology contains properties derived from 'hasLinkTo' that allow circular references "
425
+ "between resources. This is not a problem in itself, but if you try to upload data that actually "
426
+ "contains circular references, these 'hasLinkTo' properties will be temporarily removed from the "
427
+ "affected resources. Therefore, it is necessary that all involved 'hasLinkTo' properties have a "
428
+ "cardinality of 0-1 or 0-n. \n"
429
+ "Please make sure that the following properties have a cardinality of 0-1 or 0-n:"
430
+ )
431
+ return CollectedProblems(msg, errors)
432
+ return None
433
+
434
+
435
+ def _collect_link_properties(project_definition: dict[Any, Any]) -> dict[str, list[str]]:
436
+ """
437
+ Maps the properties derived from hasLinkTo to the resource classes they point to.
438
+ Args:
439
+ project_definition: parsed JSON file
440
+ Returns:
441
+ A (possibly empty) dictionary in the form {"rosetta:hasImage2D": ["rosetta:Image2D"], ...}
442
+ """
443
+ ontos = project_definition["project"]["ontologies"]
444
+ hasLinkTo_props = {"hasLinkTo", "isPartOf", "isRegionOf"}
445
+ link_properties: dict[str, list[str]] = {}
446
+ for index, onto in enumerate(ontos):
447
+ hasLinkTo_matches = []
448
+ # look for child-properties down to 5 inheritance levels that are derived from hasLinkTo-properties
449
+ for _ in range(5):
450
+ for hasLinkTo_prop in hasLinkTo_props:
451
+ hasLinkTo_matches.extend(
452
+ jsonpath_ng.ext.parse(
453
+ f"$.project.ontologies[{index}].properties[?super[*] == {hasLinkTo_prop}]"
454
+ ).find(project_definition)
455
+ )
456
+ # make the children from this iteration to the parents of the next iteration
457
+ hasLinkTo_props = {x.value["name"] for x in hasLinkTo_matches}
458
+ prop_obj_pair: dict[str, list[str]] = {}
459
+ for match in hasLinkTo_matches:
460
+ prop = onto["name"] + ":" + match.value["name"]
461
+ target = match.value["object"]
462
+ if target != "Resource":
463
+ # make the target a fully qualified name (with the ontology's name prefixed)
464
+ target = regex.sub(r"^:([^:]+)$", f"{onto['name']}:\\1", target)
465
+ prop_obj_pair[prop] = [target]
466
+ link_properties.update(prop_obj_pair)
467
+
468
+ # in case the object of a property is "Resource", the link can point to any resource class
469
+ all_res_names: list[str] = []
470
+ for onto in ontos:
471
+ matches = jsonpath_ng.ext.parse("$.resources[*].name").find(onto)
472
+ tmp = [f"{onto['name']}:{match.value}" for match in matches]
473
+ all_res_names.extend(tmp)
474
+ for prop, targ in link_properties.items():
475
+ if "Resource" in targ:
476
+ link_properties[prop] = all_res_names
477
+
478
+ return link_properties
479
+
480
+
481
+ def _identify_problematic_cardinalities(
482
+ project_definition: dict[Any, Any],
483
+ link_properties: dict[str, list[str]],
484
+ ) -> list[CreateProblem]:
485
+ """
486
+ Make an error list with all cardinalities that are part of a circle but have a cardinality of "1" or "1-n".
487
+ Args:
488
+ project_definition: parsed JSON file
489
+ link_properties: mapping of hasLinkTo-properties to classes they point to,
490
+ e.g. {"rosetta:hasImage2D": ["rosetta:Image2D"], ...}
491
+ Returns:
492
+ a (possibly empty) list of (resource, problematic_cardinality) tuples
493
+ """
494
+ cardinalities, dependencies = _extract_cardinalities_from_project(project_definition, link_properties)
495
+ graph = _make_cardinality_dependency_graph(dependencies)
496
+ errors = _find_circles_with_min_one_cardinality(graph, cardinalities, dependencies)
497
+ return [
498
+ InputProblem(f"Class: {res} / Property: {prop}", InputProblemType.MIN_CARDINALITY_ONE_WITH_CIRCLE)
499
+ for (res, prop) in errors
500
+ ]
501
+
502
+
503
+ def _extract_cardinalities_from_project(
504
+ project_definition: dict[Any, Any],
505
+ link_properties: dict[str, list[str]],
506
+ ) -> tuple[dict[str, dict[str, str]], dict[str, dict[str, list[str]]]]:
507
+ # dependencies = {"rosetta:Text": {"rosetta:hasImage2D": ["rosetta:Image2D"], ...}}
508
+ dependencies: dict[str, dict[str, list[str]]] = {}
509
+ # cardinalities = {"rosetta:Text": {"rosetta:hasImage2D": "0-1", ...}}
510
+ cardinalities: dict[str, dict[str, str]] = {}
511
+
512
+ for onto in project_definition["project"]["ontologies"]:
513
+ for resource in onto["resources"]:
514
+ resname: str = onto["name"] + ":" + resource["name"]
515
+ for card in resource.get("cardinalities", []):
516
+ # make the cardinality a fully qualified name (with the ontology's name prefixed)
517
+ cardname = regex.sub(r"^(:?)([^:]+)$", f"{onto['name']}:\\2", card["propname"])
518
+ if cardname in link_properties:
519
+ # Look out: if `targets` is created with `targets = link_properties[cardname]`, the ex-
520
+ # pression `dependencies[resname][cardname] = targets` causes `dependencies[resname][cardname]`
521
+ # to point to `link_properties[cardname]`. Due to that, the expression
522
+ # `dependencies[resname][cardname].extend(targets)` will modify "link_properties"!
523
+ # For this reason, `targets` must be created with `targets = list(link_properties[cardname])`
524
+ targets = list(link_properties[cardname])
525
+ if resname not in dependencies:
526
+ dependencies[resname] = {cardname: targets}
527
+ cardinalities[resname] = {cardname: card["cardinality"]}
528
+ elif cardname not in dependencies[resname]:
529
+ dependencies[resname][cardname] = targets
530
+ cardinalities[resname][cardname] = card["cardinality"]
531
+ else:
532
+ dependencies[resname][cardname].extend(targets)
533
+ return cardinalities, dependencies
534
+
535
+
536
+ def _make_cardinality_dependency_graph(dependencies: dict[str, dict[str, list[str]]]) -> nx.MultiDiGraph[Any]:
537
+ graph: nx.MultiDiGraph[Any] = nx.MultiDiGraph()
538
+ for start, cards in dependencies.items():
539
+ for edge, targets in cards.items():
540
+ for target in targets:
541
+ graph.add_edge(start, target, edge)
542
+ return graph
543
+
544
+
545
+ def _find_circles_with_min_one_cardinality(
546
+ graph: nx.MultiDiGraph[Any], cardinalities: dict[str, dict[str, str]], dependencies: dict[str, dict[str, list[str]]]
547
+ ) -> set[tuple[str, str]]:
548
+ errors: set[tuple[str, str]] = set()
549
+ circles = list(nx.algorithms.cycles.simple_cycles(graph))
550
+ for circle in circles:
551
+ for index, resource in enumerate(circle):
552
+ target = circle[(index + 1) % len(circle)]
553
+ prop = ""
554
+ for _property, targets in dependencies[resource].items():
555
+ if target in targets:
556
+ prop = _property
557
+ if cardinalities[resource].get(prop) not in ["0-1", "0-n"]:
558
+ errors.add((resource, prop))
559
+ return errors
560
+
561
+
562
+ def _check_for_duplicate_res_and_props(project_definition: dict[str, Any]) -> list[CollectedProblems]:
563
+ propnames_duplicates, resnames_duplicates = _find_duplicate_res_and_props(project_definition)
564
+ problems: list[CollectedProblems] = []
565
+
566
+ res_problems: list[CreateProblem] = []
567
+ for ontoname, res_duplicates in resnames_duplicates.items():
568
+ res_problems.extend(
569
+ [InputProblem(f"{ontoname}:{dup}", InputProblemType.DUPLICATE_CLASS_NAME) for dup in res_duplicates]
570
+ )
571
+ if res_problems:
572
+ problems.append(
573
+ CollectedProblems("The following class names appear multiple times in one ontology:", res_problems)
574
+ )
575
+
576
+ prop_problems: list[CreateProblem] = []
577
+ for ontoname, prop_duplicates in propnames_duplicates.items():
578
+ prop_problems.extend(
579
+ [InputProblem(f"{ontoname}:{dup}", InputProblemType.DUPLICATE_PROPERTY_NAME) for dup in prop_duplicates]
580
+ )
581
+ if prop_problems:
582
+ problems.append(
583
+ CollectedProblems("The following property names appear multiple times in one ontology:", prop_problems)
584
+ )
585
+
586
+ return problems
587
+
588
+
589
+ def _check_for_duplicate_listnodes(lists_section: list[dict[str, Any]]) -> None | CollectedProblems:
590
+ if listnode_duplicates := _find_duplicate_listnodes(lists_section):
591
+ return CollectedProblems(
592
+ "The following list node names are used multiple times in your project:",
593
+ [InputProblem(", ".join(listnode_duplicates), InputProblemType.DUPLICATE_LIST_NAME)],
594
+ )
595
+ return None