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,233 @@
1
+ from dataclasses import dataclass
2
+ from dataclasses import field
3
+ from functools import partial
4
+ from importlib.metadata import version
5
+ from typing import Any
6
+ from typing import Literal
7
+ from typing import cast
8
+
9
+ import regex
10
+ from requests import ReadTimeout
11
+ from requests import RequestException
12
+ from requests import Response
13
+ from requests import Session
14
+
15
+ from dsp_tools.clients.authentication_client import AuthenticationClient
16
+ from dsp_tools.clients.connection import Connection
17
+ from dsp_tools.config.logger_config import WARNINGS_SAVEPATH
18
+ from dsp_tools.error.exceptions import InvalidInputError
19
+ from dsp_tools.error.exceptions import PermanentConnectionError
20
+ from dsp_tools.utils.request_utils import PostFiles
21
+ from dsp_tools.utils.request_utils import RequestParameters
22
+ from dsp_tools.utils.request_utils import log_and_raise_timeouts
23
+ from dsp_tools.utils.request_utils import log_request
24
+ from dsp_tools.utils.request_utils import log_request_failure_and_sleep
25
+ from dsp_tools.utils.request_utils import log_response
26
+ from dsp_tools.utils.request_utils import should_retry
27
+
28
+ HTTP_OK = 200
29
+
30
+
31
+ @dataclass
32
+ class ConnectionLive(Connection):
33
+ """
34
+ A Connection instance represents a connection to a DSP server.
35
+
36
+ Attributes:
37
+ server: address of the server, e.g https://api.dasch.swiss
38
+ token: session token received by the server after login
39
+ """
40
+
41
+ server: str
42
+ authenticationClient: AuthenticationClient | None = None
43
+ session: Session = field(init=False, default=Session())
44
+ # downtimes of server-side services -> API still processes request
45
+ # -> retry too early has side effects (e.g. duplicated resources)
46
+ timeout_put_post: int = field(init=False, default=30 * 60)
47
+ timeout_get: int = field(init=False, default=20)
48
+
49
+ def __post_init__(self) -> None:
50
+ self.session.headers["User-Agent"] = f"DSP-TOOLS/{version('dsp-tools')}"
51
+ if self.server.endswith("/"):
52
+ self.server = self.server[:-1]
53
+ if self.authenticationClient and (token := self.authenticationClient.get_token()):
54
+ self.session.headers["Authorization"] = f"Bearer {token}"
55
+
56
+ def post(
57
+ self,
58
+ route: str,
59
+ data: dict[str, Any] | None = None,
60
+ files: PostFiles | None = None,
61
+ headers: dict[str, str] | None = None,
62
+ timeout: int | None = None,
63
+ ) -> dict[str, Any]:
64
+ """
65
+ Make an HTTP POST request to the server to which this connection has been established.
66
+
67
+ Args:
68
+ route: route that will be called on the server
69
+ data: payload of the HTTP request
70
+ files: files to be uploaded, if any
71
+ headers: headers for the HTTP request
72
+ timeout: timeout of the HTTP request, or None if the default should be used
73
+
74
+ Returns:
75
+ response from server
76
+
77
+ Raises:
78
+ PermanentConnectionError: if all attempts have failed
79
+ InvalidInputError: if the API responds with a permanent error because of invalid input data
80
+ """
81
+ if data:
82
+ headers = headers or {}
83
+ if "Content-Type" not in headers:
84
+ headers["Content-Type"] = "application/json; charset=UTF-8"
85
+ params = RequestParameters(
86
+ "POST", self._make_url(route), timeout or self.timeout_put_post, data, headers, files
87
+ )
88
+ response = self._try_network_action(params)
89
+ return cast(dict[str, Any], response.json())
90
+
91
+ def get(
92
+ self,
93
+ route: str,
94
+ headers: dict[str, str] | None = None,
95
+ ) -> dict[str, Any]:
96
+ """
97
+ Make an HTTP GET request to the server to which this connection has been established.
98
+
99
+ Args:
100
+ route: route that will be called on the server
101
+ headers: headers for the HTTP request
102
+
103
+ Returns:
104
+ response from server
105
+
106
+ Raises:
107
+ PermanentConnectionError: if all attempts have failed
108
+ InvalidInputError: if the API responds with a permanent error because of invalid input data
109
+ """
110
+ params = RequestParameters("GET", self._make_url(route), self.timeout_get, headers=headers)
111
+ response = self._try_network_action(params)
112
+ return cast(dict[str, Any], response.json())
113
+
114
+ def put(
115
+ self,
116
+ route: str,
117
+ data: dict[str, Any] | None = None,
118
+ headers: dict[str, str] | None = None,
119
+ ) -> dict[str, Any]:
120
+ """
121
+ Make an HTTP GET request to the server to which this connection has been established.
122
+
123
+ Args:
124
+ route: route that will be called on the server
125
+ data: payload of the HTTP request
126
+ headers: headers of the HTTP request
127
+
128
+ Returns:
129
+ response from server
130
+
131
+ Raises:
132
+ PermanentConnectionError: if all attempts have failed
133
+ InvalidInputError: if the API responds with a permanent error because of invalid input data
134
+ """
135
+ if data:
136
+ headers = headers or {}
137
+ if "Content-Type" not in headers:
138
+ headers["Content-Type"] = "application/json; charset=UTF-8"
139
+ params = RequestParameters("PUT", self._make_url(route), self.timeout_put_post, data, headers)
140
+ response = self._try_network_action(params)
141
+ return cast(dict[str, Any], response.json())
142
+
143
+ def _make_url(self, route: str) -> str:
144
+ if not route.startswith("/"):
145
+ route = f"/{route}"
146
+ return self.server + route
147
+
148
+ def _try_network_action(self, params: RequestParameters) -> Response:
149
+ """
150
+ Try several times to execute an HTTP request.
151
+ If a timeout error, a ConnectionError, or a requests.RequestException occur,
152
+ or if the response indicates that there is a non-permanent server-side problem,
153
+ this function waits and retries the HTTP request.
154
+
155
+ Args:
156
+ params: keyword arguments for the HTTP request
157
+
158
+ Raises:
159
+ BadCredentialsError: if the server returns a 401 status code on the route /v2/authentication
160
+ PermanentConnectionError: if all attempts have failed
161
+ InvalidInputError: if the API responds with a permanent error because of invalid input data
162
+ unexpected exceptions: if the action fails with an unexpected exception
163
+
164
+ Returns:
165
+ the return value of action
166
+ """
167
+ action = partial(self.session.request, **params.as_kwargs())
168
+ num_of_retries = 24 # xmlupload must handle > 45 min fuseki downtime due to compaction, see DEV-5089
169
+ for retry_counter in range(num_of_retries):
170
+ try:
171
+ log_request(params, dict(self.session.headers))
172
+ response = action()
173
+ except (TimeoutError, ReadTimeout) as err:
174
+ log_and_raise_timeouts(err)
175
+ except (ConnectionError, RequestException):
176
+ self._renew_session()
177
+ log_request_failure_and_sleep(
178
+ reason="Connection Error raised", retry_counter=retry_counter, exc_info=True
179
+ )
180
+ continue
181
+
182
+ log_response(response)
183
+ if response.status_code == HTTP_OK:
184
+ return response
185
+
186
+ self._handle_non_ok_responses(response, retry_counter)
187
+
188
+ # if all attempts have failed, raise error
189
+ msg = f"Permanently unable to execute the network action. See {WARNINGS_SAVEPATH} for more information."
190
+ raise PermanentConnectionError(msg)
191
+
192
+ def _handle_non_ok_responses(self, response: Response, retry_counter: int) -> None:
193
+ if should_retry(response):
194
+ log_request_failure_and_sleep("Transient Error", retry_counter, exc_info=False)
195
+ return None
196
+ api_msg = self._extract_original_api_err_msg(str(response.content))
197
+ blame = self._determine_blame(api_msg)
198
+ if blame == "client":
199
+ raise InvalidInputError(api_msg)
200
+ else:
201
+ msg = f"Permanently unable to execute the network action.\n{' ' * 37}Original Message: {api_msg}\n"
202
+ raise PermanentConnectionError(msg)
203
+
204
+ def _extract_original_api_err_msg(self, response_content: str) -> str:
205
+ if found := regex.search(r'{"knora-api:error":"dsp\.errors\.(.*)","@context', response_content):
206
+ api_msg = found.group(1)
207
+ if found := regex.search(r'{"message":"(.+)"}', response_content):
208
+ api_msg = found.group(1)
209
+ else:
210
+ api_msg = str(response_content)
211
+ return api_msg
212
+
213
+ def _determine_blame(self, api_msg: str) -> Literal["server", "client"]:
214
+ api_msg = api_msg.lower()
215
+ client_markers = [
216
+ "OntologyConstraintException",
217
+ "NotFoundException",
218
+ "One or more resources were not found",
219
+ "does not allow more than one value for property",
220
+ "Duplicate values for property",
221
+ "Text value contains invalid characters",
222
+ ]
223
+ blame: Literal["server", "client"] = "server"
224
+ if any(x.lower() in api_msg for x in client_markers):
225
+ blame = "client"
226
+ return blame
227
+
228
+ def _renew_session(self) -> None:
229
+ self.session.close()
230
+ self.session = Session()
231
+ self.session.headers["User-Agent"] = f"DSP-TOOLS/{version('dsp-tools')}"
232
+ if self.authenticationClient and (token := self.authenticationClient.get_token()):
233
+ self.session.headers["Authorization"] = f"Bearer {token}"
@@ -0,0 +1,60 @@
1
+ import shlex
2
+ import subprocess
3
+ from dataclasses import dataclass
4
+ from enum import Enum
5
+ from enum import auto
6
+
7
+ from loguru import logger
8
+
9
+
10
+ class FusekiBloatingLevel(Enum):
11
+ OK = auto()
12
+ WARNING = auto()
13
+ CRITICAL = auto()
14
+ CALCULATION_FAILURE = auto()
15
+
16
+
17
+ @dataclass
18
+ class FusekiMetrics:
19
+ container_id: str | None = None
20
+ start_size: int | None = None
21
+ end_size: int | None = None
22
+
23
+ def try_get_start_size(self) -> None:
24
+ self.start_size = self._try_get_size()
25
+
26
+ def try_get_end_size(self) -> None:
27
+ self.end_size = self._try_get_size()
28
+
29
+ def _try_get_size(self) -> int | None:
30
+ if not self.container_id:
31
+ self._try_get_container_id()
32
+ if not self.container_id:
33
+ return None
34
+ if result := self._run_command(["docker", "exec", self.container_id, "du", "-sb", "/fuseki"]):
35
+ try:
36
+ size_str = result.split()[0]
37
+ return int(size_str)
38
+ except (ValueError, IndexError):
39
+ logger.error("Could not parse size from du command output.")
40
+ return None
41
+ return None
42
+
43
+ def _try_get_container_id(self) -> None:
44
+ if result := self._run_command(["docker", "ps", "--format", "{{.ID}} {{.Image}}"]):
45
+ for line in result.splitlines():
46
+ parts = shlex.split(line)
47
+ if len(parts) == 2 and "daschswiss/apache-jena-fuseki" in parts[1]:
48
+ self.container_id = parts[0]
49
+ return
50
+ logger.error("Could not find Fuseki container ID.")
51
+
52
+ def _run_command(self, cmd: list[str]) -> str | None:
53
+ logger.debug(f"Run command: {cmd}")
54
+ result = subprocess.run(cmd, check=False, capture_output=True, text=True)
55
+ result_str = f"Result code: {result.returncode}, Message: {result.stdout}"
56
+ if result.returncode != 0:
57
+ logger.error(f"Could not run command: {cmd}. {result_str}")
58
+ return None
59
+ logger.debug(f"Command output: {result_str}")
60
+ return result.stdout.strip()
@@ -0,0 +1,35 @@
1
+ from typing import Any
2
+ from typing import Protocol
3
+
4
+ from dsp_tools.clients.authentication_client import AuthenticationClient
5
+
6
+
7
+ class GroupClient(Protocol):
8
+ api_url: str
9
+ auth: AuthenticationClient
10
+
11
+ def get_all_groups(self) -> list[dict[str, Any]]:
12
+ """Get all the groups on this DSP-Server."""
13
+
14
+ def create_new_group(self, group_dict: dict[str, Any]) -> str | None:
15
+ """Create a new group."""
16
+
17
+
18
+ class UserClient(Protocol):
19
+ api_url: str
20
+ auth: AuthenticationClient
21
+
22
+ def get_user_iri_by_username(self, username: str) -> str | None:
23
+ """Get a user by its username."""
24
+
25
+ def post_new_user(self, user_dict: dict[str, Any]) -> str | None:
26
+ """Create a new user."""
27
+
28
+ def add_user_as_project_member(self, user_iri: str, project_iri: str) -> bool:
29
+ """Add an existing user to a project."""
30
+
31
+ def add_user_as_project_admin(self, user_iri: str, project_iri: str) -> bool:
32
+ """Add a user as a project admin."""
33
+
34
+ def add_user_to_custom_groups(self, user_iri: str, groups: list[str]) -> bool:
35
+ """Add a user to a custom group."""
@@ -0,0 +1,181 @@
1
+ from dataclasses import dataclass
2
+ from http import HTTPStatus
3
+ from typing import Any
4
+ from typing import cast
5
+ from urllib.parse import quote_plus
6
+
7
+ import requests
8
+ from requests import RequestException
9
+
10
+ from dsp_tools.clients.authentication_client import AuthenticationClient
11
+ from dsp_tools.clients.group_user_clients import GroupClient
12
+ from dsp_tools.clients.group_user_clients import UserClient
13
+ from dsp_tools.error.exceptions import BadCredentialsError
14
+ from dsp_tools.error.exceptions import FatalNonOkApiResponseCode
15
+ from dsp_tools.utils.request_utils import RequestParameters
16
+ from dsp_tools.utils.request_utils import log_and_raise_request_exception
17
+ from dsp_tools.utils.request_utils import log_and_warn_unexpected_non_ok_response
18
+ from dsp_tools.utils.request_utils import log_request
19
+ from dsp_tools.utils.request_utils import log_response
20
+
21
+ TIMEOUT = 30
22
+
23
+
24
+ @dataclass
25
+ class UserClientLive(UserClient):
26
+ api_url: str
27
+ auth: AuthenticationClient
28
+
29
+ def get_user_iri_by_username(self, username: str) -> str | None:
30
+ url = f"{self.api_url}/admin/users/username/{username}"
31
+ headers = {"Authorization": f"Bearer {self.auth.get_token()}"}
32
+ params = RequestParameters("GET", url, TIMEOUT, headers=headers)
33
+ log_request(params)
34
+ try:
35
+ response = requests.get(url=params.url, headers=params.headers, timeout=params.timeout)
36
+ except RequestException as err:
37
+ log_and_raise_request_exception(err)
38
+ log_response(response)
39
+ if response.ok:
40
+ result = response.json()
41
+ return cast(str, result["user"]["id"])
42
+ if response.status_code == HTTPStatus.NOT_FOUND:
43
+ return None
44
+ if response.status_code == HTTPStatus.FORBIDDEN:
45
+ raise BadCredentialsError("You do not have sufficient credentials to retrieve user information.")
46
+ log_and_warn_unexpected_non_ok_response(response.status_code, response.text)
47
+ return None
48
+
49
+ def post_new_user(self, user_dict: dict[str, Any]) -> str | None:
50
+ url = f"{self.api_url}/admin/users"
51
+ headers = {
52
+ "Content-Type": "application/json",
53
+ "Authorization": f"Bearer {self.auth.get_token()}",
54
+ }
55
+ params = RequestParameters("POST", url, TIMEOUT, data=user_dict, headers=headers)
56
+ log_request(params)
57
+ try:
58
+ response = requests.post(
59
+ url=params.url, headers=params.headers, data=params.data_serialized, timeout=params.timeout
60
+ )
61
+ except RequestException as err:
62
+ log_and_raise_request_exception(err)
63
+ log_response(response)
64
+ if response.ok:
65
+ result = response.json()
66
+ return cast(str, result["user"]["id"])
67
+ if response.status_code == HTTPStatus.BAD_REQUEST:
68
+ # if the user exists we get a 400, this should never happen through code design
69
+ raise FatalNonOkApiResponseCode(url, response.status_code, response.text)
70
+ if response.status_code == HTTPStatus.FORBIDDEN:
71
+ raise BadCredentialsError("You don't have permission to create users.")
72
+ log_and_warn_unexpected_non_ok_response(response.status_code, response.text)
73
+ return None
74
+
75
+ def add_user_as_project_member(self, user_iri: str, project_iri: str) -> bool:
76
+ project_iri_encoded = quote_plus(project_iri)
77
+ user_iri_encoded = quote_plus(user_iri)
78
+ url = f"{self.api_url}/admin/users/iri/{user_iri_encoded}/project-memberships/{project_iri_encoded}"
79
+ headers = {"Authorization": f"Bearer {self.auth.get_token()}"}
80
+ params = RequestParameters("POST", url, TIMEOUT, headers=headers)
81
+ log_request(params)
82
+ try:
83
+ response = requests.post(url=params.url, headers=params.headers, timeout=params.timeout)
84
+ except RequestException as err:
85
+ log_and_raise_request_exception(err)
86
+ log_response(response)
87
+ if response.ok:
88
+ return True
89
+ if response.status_code == HTTPStatus.FORBIDDEN:
90
+ raise BadCredentialsError("You don't have permission to add users to projects.")
91
+ log_and_warn_unexpected_non_ok_response(response.status_code, response.text)
92
+ return False
93
+
94
+ def add_user_as_project_admin(self, user_iri: str, project_iri: str) -> bool:
95
+ project_iri_encoded = quote_plus(project_iri)
96
+ user_iri_encoded = quote_plus(user_iri)
97
+ url = f"{self.api_url}/admin/users/iri/{user_iri_encoded}/project-admin-memberships/{project_iri_encoded}"
98
+ headers = {"Authorization": f"Bearer {self.auth.get_token()}"}
99
+ params = RequestParameters("POST", url, TIMEOUT, headers=headers)
100
+ log_request(params)
101
+ try:
102
+ response = requests.post(url=params.url, headers=params.headers, timeout=params.timeout)
103
+ except RequestException as err:
104
+ log_and_raise_request_exception(err)
105
+ log_response(response)
106
+ if response.ok:
107
+ return True
108
+ if response.status_code == HTTPStatus.FORBIDDEN:
109
+ raise BadCredentialsError("You don't have permission to add users as project admins.")
110
+ log_and_warn_unexpected_non_ok_response(response.status_code, response.text)
111
+ return False
112
+
113
+ def add_user_to_custom_groups(self, user_iri: str, groups: list[str]) -> bool:
114
+ user_iri_encoded = quote_plus(user_iri)
115
+ successes = []
116
+ for gr in groups:
117
+ result = self._add_user_to_one_group(user_iri_encoded, gr)
118
+ successes.append(result)
119
+ return all(successes)
120
+
121
+ def _add_user_to_one_group(self, user_iri_encoded: str, group_iri: str) -> bool:
122
+ group_iri_encoded = quote_plus(group_iri)
123
+ url = f"{self.api_url}/admin/users/iri/{user_iri_encoded}/group-memberships/{group_iri_encoded}"
124
+ headers = {"Authorization": f"Bearer {self.auth.get_token()}"}
125
+ params = RequestParameters("POST", url, TIMEOUT, headers=headers)
126
+ log_request(params)
127
+ try:
128
+ response = requests.post(url=params.url, headers=params.headers, timeout=params.timeout)
129
+ except RequestException as err:
130
+ log_and_raise_request_exception(err)
131
+ log_response(response)
132
+ if response.ok:
133
+ return True
134
+ if response.status_code == HTTPStatus.FORBIDDEN:
135
+ raise BadCredentialsError("You don't have permission to add users to groups.")
136
+ log_and_warn_unexpected_non_ok_response(response.status_code, response.text)
137
+ return False
138
+
139
+
140
+ @dataclass
141
+ class GroupClientLive(GroupClient):
142
+ api_url: str
143
+ auth: AuthenticationClient
144
+
145
+ def get_all_groups(self) -> list[dict[str, Any]]:
146
+ url = f"{self.api_url}/admin/groups"
147
+ params = RequestParameters("GET", url, TIMEOUT)
148
+ log_request(params)
149
+ try:
150
+ response = requests.get(params.url, timeout=params.timeout)
151
+ except RequestException as err:
152
+ log_and_raise_request_exception(err)
153
+ log_response(response)
154
+ if response.ok:
155
+ result = response.json()
156
+ return cast(list[dict[str, Any]], result["groups"])
157
+ log_and_warn_unexpected_non_ok_response(response.status_code, response.text)
158
+ return []
159
+
160
+ def create_new_group(self, group_dict: dict[str, Any]) -> str | None:
161
+ url = f"{self.api_url}/admin/groups"
162
+ headers = {"Accept": "application/json", "Authorization": f"Bearer {self.auth.get_token()}"}
163
+ params = RequestParameters("POST", url, TIMEOUT, headers=headers, data=group_dict)
164
+ log_request(params)
165
+ try:
166
+ response = requests.post(
167
+ params.url, data=params.data_serialized, timeout=params.timeout, headers=params.headers
168
+ )
169
+ except RequestException as err:
170
+ log_and_raise_request_exception(err)
171
+ log_response(response)
172
+ if response.ok:
173
+ result = response.json()
174
+ return cast(str, result["group"]["id"])
175
+ if response.status_code == HTTPStatus.FORBIDDEN:
176
+ raise BadCredentialsError(
177
+ "Only a SystemAdmin or ProjectAdmin can create groups. "
178
+ "Your permissions are insufficient for this action."
179
+ )
180
+ log_and_warn_unexpected_non_ok_response(response.status_code, response.text)
181
+ return None
@@ -0,0 +1,23 @@
1
+ from typing import Any
2
+ from typing import Protocol
3
+
4
+ from dsp_tools.clients.authentication_client import AuthenticationClient
5
+
6
+
7
+ class LegalInfoClient(Protocol):
8
+ """
9
+ Protocol class/interface for the legal info endpoint of the admin API.
10
+ """
11
+
12
+ server: str
13
+ project_shortcode: str
14
+ authentication_client: AuthenticationClient
15
+
16
+ def post_copyright_holders(self, copyright_holders: list[str]) -> None:
17
+ """Send a list of new copyright holders to the API"""
18
+
19
+ def get_licenses_of_a_project(self, enabled_only: bool) -> list[dict[str, Any]]:
20
+ """Get a list of enabled licenses for the project."""
21
+
22
+ def enable_unknown_license(self) -> None:
23
+ """Enable the license http://rdfh.ch/licenses/unknown"""
@@ -0,0 +1,132 @@
1
+ from dataclasses import dataclass
2
+ from http import HTTPStatus
3
+ from typing import Any
4
+
5
+ import requests
6
+ from loguru import logger
7
+ from requests import RequestException
8
+ from requests import Response
9
+
10
+ from dsp_tools.clients.authentication_client import AuthenticationClient
11
+ from dsp_tools.clients.legal_info_client import LegalInfoClient
12
+ from dsp_tools.error.exceptions import BadCredentialsError
13
+ from dsp_tools.error.exceptions import FatalNonOkApiResponseCode
14
+ from dsp_tools.utils.request_utils import RequestParameters
15
+ from dsp_tools.utils.request_utils import log_and_raise_request_exception
16
+ from dsp_tools.utils.request_utils import log_request
17
+ from dsp_tools.utils.request_utils import log_response
18
+
19
+ TIMEOUT = 60
20
+
21
+
22
+ @dataclass
23
+ class LegalInfoClientLive(LegalInfoClient):
24
+ server: str
25
+ project_shortcode: str
26
+ authentication_client: AuthenticationClient
27
+
28
+ def post_copyright_holders(self, copyright_holders: list[str]) -> None:
29
+ """Send a list of new copyright holders to the API"""
30
+ logger.debug(f"POST {len(copyright_holders)} new copyright holders")
31
+ url = f"{self.server}/admin/projects/shortcode/{self.project_shortcode}/legal-info/copyright-holders"
32
+ try:
33
+ response = self._post_and_log_request(url, copyright_holders)
34
+ except RequestException as err:
35
+ log_and_raise_request_exception(err)
36
+ if response.ok:
37
+ return
38
+ if response.status_code == HTTPStatus.FORBIDDEN:
39
+ raise BadCredentialsError(
40
+ "Only a SystemAdmin or ProjectAdmin can create new copyright holders. "
41
+ "Your permissions are insufficient for this action."
42
+ )
43
+ raise FatalNonOkApiResponseCode(url, response.status_code, response.text)
44
+
45
+ def _post_and_log_request(self, url: str, data: list[str]) -> Response:
46
+ headers = {
47
+ "Content-Type": "application/json",
48
+ "Authorization": f"Bearer {self.authentication_client.get_token()}",
49
+ }
50
+ params = RequestParameters("POST", url, TIMEOUT, {"data": data}, headers)
51
+ log_request(params)
52
+ response = requests.post(
53
+ url=params.url,
54
+ headers=params.headers,
55
+ data=params.data_serialized,
56
+ timeout=params.timeout,
57
+ )
58
+ log_response(response)
59
+ return response
60
+
61
+ def get_licenses_of_a_project(self, enabled_only: bool = True) -> list[dict[str, Any]]:
62
+ logger.debug("GET enabled licenses of the project.")
63
+ page_num = 1
64
+ all_data = []
65
+ is_last_page = False
66
+ while not is_last_page:
67
+ response = self._get_one_license_page(page_num, enabled_only)
68
+ response_dict = response.json()
69
+ all_data.extend(response_dict["data"])
70
+ is_last_page = _is_last_page(response_dict)
71
+ return all_data
72
+
73
+ def _get_one_license_page(self, page_num: int, enabled_only: bool) -> Response:
74
+ enabled = str(enabled_only).lower()
75
+ url = (
76
+ f"{self.server}/admin/projects/shortcode/{self.project_shortcode}/"
77
+ f"legal-info/licenses?page={page_num}&page-size=25&order=Asc&showOnlyEnabled={enabled}"
78
+ )
79
+ headers = {
80
+ "Content-Type": "application/json",
81
+ "Authorization": f"Bearer {self.authentication_client.get_token()}",
82
+ }
83
+ params = RequestParameters(method="GET", url=url, timeout=TIMEOUT, headers=headers)
84
+ log_request(params)
85
+ try:
86
+ response = requests.get(
87
+ url=params.url,
88
+ headers=params.headers,
89
+ timeout=params.timeout,
90
+ )
91
+ log_response(response)
92
+ except RequestException as err:
93
+ log_and_raise_request_exception(err)
94
+ if response.ok:
95
+ return response
96
+ raise FatalNonOkApiResponseCode(url, response.status_code, response.text)
97
+
98
+ def enable_unknown_license(self) -> None:
99
+ escaped_license_iri = "http%3A%2F%2Frdfh.ch%2Flicenses%2Funknown"
100
+ url = (
101
+ f"{self.server}/admin/projects/shortcode/{self.project_shortcode}/"
102
+ f"legal-info/licenses/{escaped_license_iri}/enable"
103
+ )
104
+ headers = {
105
+ "Content-Type": "application/json",
106
+ "Authorization": f"Bearer {self.authentication_client.get_token()}",
107
+ }
108
+ params = RequestParameters("POST", url, TIMEOUT, headers=headers)
109
+ log_request(params)
110
+ try:
111
+ response = requests.put(
112
+ url=params.url,
113
+ headers=params.headers,
114
+ timeout=params.timeout,
115
+ )
116
+ log_response(response)
117
+ except RequestException as err:
118
+ log_and_raise_request_exception(err)
119
+ if response.ok:
120
+ return
121
+ if response.status_code == HTTPStatus.FORBIDDEN:
122
+ raise BadCredentialsError(
123
+ "Only a SystemAdmin or ProjectAdmin can enable licenses. "
124
+ "Your permissions are insufficient for this action."
125
+ )
126
+ raise FatalNonOkApiResponseCode(url, response.status_code, response.text)
127
+
128
+
129
+ def _is_last_page(response: dict[str, Any]) -> bool:
130
+ current_page = response["pagination"]["currentPage"]
131
+ total_page = response["pagination"]["totalPages"]
132
+ return bool(current_page == total_page)