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
@@ -1,250 +1,518 @@
1
1
  from __future__ import annotations
2
2
 
3
- import warnings
3
+ from collections.abc import Collection
4
4
  from dataclasses import dataclass
5
- from dataclasses import field
6
5
  from typing import Any
7
6
 
8
- from lxml import etree
9
-
10
- from dsp_tools.models.custom_warnings import DspToolsUserWarning
11
- from dsp_tools.models.exceptions import InputError
12
- from dsp_tools.xmllib.models.migration_metadata import MigrationMetadata
13
- from dsp_tools.xmllib.models.values import ColorValue
14
- from dsp_tools.xmllib.models.values import LinkValue
15
- from dsp_tools.xmllib.models.values import Richtext
16
- from dsp_tools.xmllib.value_checkers import find_geometry_problem
17
- from dsp_tools.xmllib.value_checkers import is_color
7
+ from dsp_tools.error.xmllib_warnings import MessageInfo
8
+ from dsp_tools.error.xmllib_warnings_util import emit_xmllib_input_warning
9
+ from dsp_tools.xmllib.internal.input_converters import check_and_fix_collection_input
10
+ from dsp_tools.xmllib.internal.input_converters import check_and_fix_is_non_empty_string
11
+ from dsp_tools.xmllib.models.config_options import NewlineReplacement
12
+ from dsp_tools.xmllib.models.internal.geometry import Circle
13
+ from dsp_tools.xmllib.models.internal.geometry import GeometryPoint
14
+ from dsp_tools.xmllib.models.internal.geometry import GeometryShape
15
+ from dsp_tools.xmllib.models.internal.geometry import Polygon
16
+ from dsp_tools.xmllib.models.internal.geometry import Rectangle
17
+ from dsp_tools.xmllib.models.internal.geometry import Vector
18
+ from dsp_tools.xmllib.models.internal.migration_metadata import MigrationMetadata
19
+ from dsp_tools.xmllib.models.internal.values import LinkValue
20
+ from dsp_tools.xmllib.models.internal.values import Richtext
21
+ from dsp_tools.xmllib.models.internal.values import SimpleText
22
+ from dsp_tools.xmllib.models.internal.values import Value
23
+ from dsp_tools.xmllib.models.permissions import Permissions
18
24
  from dsp_tools.xmllib.value_checkers import is_decimal
19
- from dsp_tools.xmllib.value_checkers import is_string_like
20
-
21
- XML_NAMESPACE_MAP = {None: "https://dasch.swiss/schema", "xsi": "http://www.w3.org/2001/XMLSchema-instance"}
22
- DASCH_SCHEMA = "{https://dasch.swiss/schema}"
25
+ from dsp_tools.xmllib.value_checkers import is_nonempty_value
23
26
 
24
27
  LIST_SEPARATOR = "\n - "
25
28
 
26
29
 
27
30
  @dataclass
28
- class AnnotationResource:
31
+ class RegionResource:
29
32
  res_id: str
30
33
  label: str
31
- annotation_of: str
32
- comments: list[str]
33
- permissions: str = "res-default"
34
+ values: list[Value]
35
+ geometry: GeometryShape | None
36
+ permissions: Permissions = Permissions.PROJECT_SPECIFIC_PERMISSIONS
34
37
  migration_metadata: MigrationMetadata | None = None
35
38
 
36
- def __post_init__(self) -> None:
37
- _check_strings(string_to_check=self.res_id, res_id=self.res_id, field_name="Resource ID")
38
- _check_strings(string_to_check=self.label, res_id=self.res_id, field_name="Label")
39
-
40
- def new(
41
- self, res_id: str, label: str, annotation_of: str, comments: list[str], permissions: str = "res-default"
42
- ) -> AnnotationResource:
43
- return AnnotationResource(
39
+ @staticmethod
40
+ def create_new(
41
+ res_id: str,
42
+ label: str,
43
+ region_of: str,
44
+ permissions: Permissions = Permissions.PROJECT_SPECIFIC_PERMISSIONS,
45
+ ) -> RegionResource:
46
+ """
47
+ Creates a new region resource.
48
+ A region is a region of interest (ROI) in a StillImageRepresentation.
49
+
50
+ Exactly one geometry shape has to be added to the resource with one of the following methods:
51
+ `add_rectangle`, `add_polygon`, `add_circle` (see documentation below for more information).
52
+
53
+ [See XML documentation for details](https://docs.dasch.swiss/latest/DSP-TOOLS/file-formats/xml-data-file/#region)
54
+
55
+ Args:
56
+ res_id: ID of this region resource
57
+ label: label of this region resource
58
+ region_of: ID of the image resource that this region refers to (cardinality 1)
59
+ permissions: permissions of this region resource
60
+
61
+ Returns:
62
+ A region resource
63
+
64
+ Examples:
65
+ ```python
66
+ region = xmllib.RegionResource.create_new(
67
+ res_id="ID",
68
+ label="label",
69
+ region_of="image_id",
70
+ )
71
+ ```
72
+ """
73
+ _check_strings(string_to_check=res_id, res_id=res_id, field_name="Resource ID")
74
+ res_id = str(res_id)
75
+ lbl = check_and_fix_is_non_empty_string(value=label, res_id=res_id, value_field="label")
76
+ return RegionResource(
44
77
  res_id=res_id,
45
- label=label,
46
- annotation_of=annotation_of,
47
- comments=comments,
78
+ label=lbl,
79
+ values=[
80
+ LinkValue.new(
81
+ value=region_of, prop_name="isRegionOf", resource_id=res_id, comment=None, permissions=permissions
82
+ )
83
+ ],
84
+ geometry=None,
48
85
  permissions=permissions,
49
86
  )
50
87
 
51
- def add_comment(self, comment: str) -> AnnotationResource:
52
- self.comments.append(comment)
53
- return self
54
-
55
- def add_comments(self, comments: list[str]) -> AnnotationResource:
56
- self.comments.extend(comments)
57
- return self
58
-
59
- def add_migration_metadata(
60
- self, creation_date: str | None, iri: str | None = None, ark: str | None = None
61
- ) -> AnnotationResource:
62
- if self.migration_metadata:
63
- raise InputError(
64
- f"The resource with the ID '{self.res_id}' already contains migration metadata, "
65
- f"no new data can be added."
88
+ def add_rectangle(
89
+ self,
90
+ point1: tuple[float, float],
91
+ point2: tuple[float, float],
92
+ line_width: float = 2,
93
+ color: str = "#5b24bf",
94
+ active: bool = True,
95
+ ) -> RegionResource:
96
+ """
97
+ Add a rectangle shape to the region.
98
+
99
+ [For a visual example see the XML documentation](https://docs.dasch.swiss/latest/DSP-TOOLS/file-formats/xml-data-file/#geometry)
100
+
101
+ Args:
102
+ point1: first point of the rectangle represented as two numbers between 0 and 1 in the format (x, y)
103
+ point2: second point of the rectangle represented as two numbers between 0 and 1 in the format (x, y)
104
+ line_width: A number in pixels between 1 - 5
105
+ color: A hexadecimal color value which starts with a `#` followed by 3 or 6 numerals.
106
+ The default value was chosen as it is distinguishable for most color-blind people.
107
+ active: If set to `False`, the region is marked as 'deleted'
108
+
109
+ Returns:
110
+ Region with added rectangle
111
+
112
+ Examples:
113
+ ```python
114
+ region = region.add_rectangle(
115
+ point1=(0.1, 0.1),
116
+ point2=(0.2, 0.2),
117
+ )
118
+ ```
119
+
120
+ ```python
121
+ # with custom display values
122
+ region = region.add_rectangle(
123
+ point1=(0.1, 0.1),
124
+ point2=(0.2, 0.2),
125
+ line_width=3,
126
+ color="#32a873",
66
127
  )
67
- self.migration_metadata = MigrationMetadata(creation_date=creation_date, iri=iri, ark=ark, res_id=self.res_id)
128
+ ```
129
+ """
130
+ self.geometry = Rectangle(
131
+ point1=GeometryPoint(point1[0], point1[1], self.res_id),
132
+ point2=GeometryPoint(point2[0], point2[1], self.res_id),
133
+ line_width=line_width,
134
+ color=color,
135
+ active=active,
136
+ resource_id=self.res_id,
137
+ )
68
138
  return self
69
139
 
70
- def serialise(self) -> etree._Element:
71
- self.comments = _transform_unexpected_input(self.comments, "comments", self.res_id)
72
- res_ele = self._serialise_resource_element()
73
- res_ele.append(self._serialise_annotation_of())
74
- res_ele.append(_serialise_has_comment(self.comments, self.res_id))
75
- return res_ele
140
+ def add_polygon(
141
+ self,
142
+ points: list[tuple[float, float]],
143
+ line_width: float = 2,
144
+ color: str = "#5b24bf",
145
+ active: bool = True,
146
+ ) -> RegionResource:
147
+ """
148
+ Add a polygon shape to the region.
149
+ A polygon should have at least three points.
150
+ If you wish to create a rectangle, please use the designated `add_rectangle` method.
76
151
 
77
- def _serialise_resource_element(self) -> etree._Element:
78
- attribs = {"label": self.label, "id": self.res_id}
79
- if self.permissions:
80
- attribs["permissions"] = self.permissions
81
- return etree.Element(f"{DASCH_SCHEMA}annotation", attrib=attribs, nsmap=XML_NAMESPACE_MAP)
152
+ [For a visual example see the XML documentation](https://docs.dasch.swiss/latest/DSP-TOOLS/file-formats/xml-data-file/#geometry)
82
153
 
83
- def _serialise_annotation_of(self) -> etree._Element:
84
- return LinkValue(value=self.annotation_of, prop_name="isAnnotationOf", resource_id=self.res_id).serialise()
154
+ **Please note that this cannot currently be displayed in the dsp-app.**
85
155
 
156
+ Args:
157
+ points: list of tuples containing two numbers between 0 and 1 in the format (x, y)
158
+ line_width: A number in pixels between 1 - 5
159
+ color: A hexadecimal color value which starts with a `#` followed by 3 or 6 numerals.
160
+ The default value was chosen as it is distinguishable for most color-blind people.
161
+ active: If set to `False` the region is marked as 'deleted'
86
162
 
87
- @dataclass
88
- class RegionResource:
89
- res_id: str
90
- label: str
91
- color: str
92
- region_of: str
93
- geometry: dict[str, Any]
94
- comments: list[str]
95
- permissions: str = "res-default"
96
- migration_metadata: MigrationMetadata | None = None
163
+ Returns:
164
+ Region with added polygon
97
165
 
98
- def __post_init__(self) -> None:
99
- _check_strings(string_to_check=self.res_id, res_id=self.res_id, field_name="Resource ID")
100
- _check_strings(string_to_check=self.label, res_id=self.res_id, field_name="Label")
101
- if not is_color(self.color):
102
- msg = (
103
- f"The color value '{self.color}' for the resource with the ID: '{self.res_id}' failed validation. "
104
- f"Please consult the documentation for details."
166
+ Examples:
167
+ ```python
168
+ region = region.add_polygon(
169
+ points=[(0.1, 0.1), (0.2, 0.2), (0.3, 0.3)],
170
+ )
171
+ ```
172
+
173
+ ```python
174
+ # with custom display values
175
+ region = region.add_polygon(
176
+ points=[(0.1, 0.1), (0.2, 0.2), (0.3, 0.3)],
177
+ line_width=3,
178
+ color="#32a873",
105
179
  )
106
- warnings.warn(DspToolsUserWarning(msg))
107
- if fail_msg := find_geometry_problem(self.geometry):
108
- msg = f"The geometry of the resource with the ID '{self.res_id}' failed validation.\n" + fail_msg
109
- warnings.warn(DspToolsUserWarning(msg))
180
+ ```
181
+ """
182
+ geom_points = [GeometryPoint(val[0], val[1], self.res_id) for val in points]
183
+ self.geometry = Polygon(
184
+ points=geom_points, line_width=line_width, color=color, active=active, resource_id=self.res_id
185
+ )
186
+ return self
110
187
 
111
- def new(
188
+ def add_circle(
112
189
  self,
113
- res_id: str,
114
- label: str,
115
- color: str,
116
- region_of: str,
117
- geometry: dict[str, Any],
118
- comments: list[str],
119
- permissions: str = "res-default",
190
+ center: tuple[float, float],
191
+ radius: tuple[float, float],
192
+ line_width: float = 2,
193
+ color: str = "#5b24bf",
194
+ active: bool = True,
120
195
  ) -> RegionResource:
121
- return RegionResource(
122
- res_id=res_id,
123
- label=label,
196
+ """
197
+ Add a circle shape to the region.
198
+
199
+ [For a visual example see the XML documentation](https://docs.dasch.swiss/latest/DSP-TOOLS/file-formats/xml-data-file/#geometry)
200
+
201
+ **Please note that this cannot currently be displayed in the dsp-app.**
202
+
203
+ Args:
204
+ center: center of the circle, represented as two numbers between 0 and 1 in the format (x, y)
205
+ radius: radius of the circle, represented as a 2-dimensional vector,
206
+ i.e. two numbers between 0 and 1 in the format (x, y)
207
+ line_width: A number in pixels between 1 - 5
208
+ color: A hexadecimal color value which starts with a `#` followed by 3 or 6 numerals.
209
+ The default value was chosen as it is distinguishable for most color-blind people.
210
+ active: If set to `False` the region is marked as 'deleted'
211
+
212
+ Returns:
213
+ Region with added circle
214
+
215
+ Examples:
216
+ ```python
217
+ region = region.add_circle(
218
+ center=(0.1, 0.1),
219
+ radius=(0.2, 0.2),
220
+ )
221
+ ```
222
+
223
+ ```python
224
+ # with custom display values
225
+ region = region.add_circle(
226
+ center=(0.1, 0.1),
227
+ radius=(0.2, 0.2),
228
+ line_width=3,
229
+ color="#32a873",
230
+ )
231
+ ```
232
+ """
233
+ self.geometry = Circle(
234
+ center=GeometryPoint(center[0], center[1], self.res_id),
235
+ radius=Vector(radius[0], radius[1], self.res_id),
236
+ line_width=line_width,
124
237
  color=color,
125
- region_of=region_of,
126
- geometry=geometry,
127
- comments=comments,
128
- permissions=permissions,
238
+ active=active,
239
+ resource_id=self.res_id,
129
240
  )
130
-
131
- def add_comment(self, comment: str) -> RegionResource:
132
- self.comments.append(comment)
133
241
  return self
134
242
 
135
- def add_comments(self, comments: list[str]) -> RegionResource:
136
- self.comments.extend(comments)
243
+ def add_comment(
244
+ self,
245
+ text: str,
246
+ permissions: Permissions = Permissions.PROJECT_SPECIFIC_PERMISSIONS,
247
+ comment: str | None = None,
248
+ newline_replacement: NewlineReplacement = NewlineReplacement.LINEBREAK,
249
+ ) -> RegionResource:
250
+ """
251
+ Add a comment to the region
252
+
253
+ Args:
254
+ text: text of the comment
255
+ permissions: optional permissions of this value
256
+ comment: optional comment about this comment
257
+ newline_replacement: options how to deal with `\\n` inside the text value. Default: replace with `<br/>`
258
+
259
+ Returns:
260
+ The original region, with the added comment
261
+
262
+ Examples:
263
+ ```python
264
+ region = region.add_comment("comment text")
265
+ ```
266
+
267
+ ```python
268
+ region = region.add_comment(text="comment text", comment="Comment about the comment.")
269
+ ```
270
+ """
271
+ self.values.append(
272
+ Richtext.new(
273
+ value=text,
274
+ prop_name="hasComment",
275
+ permissions=permissions,
276
+ comment=comment,
277
+ resource_id=self.res_id,
278
+ newline_replacement=newline_replacement,
279
+ )
280
+ )
137
281
  return self
138
282
 
139
- def add_migration_metadata(
140
- self, creation_date: str | None, iri: str | None = None, ark: str | None = None
283
+ def add_comment_multiple(
284
+ self,
285
+ texts: Collection[str],
286
+ permissions: Permissions = Permissions.PROJECT_SPECIFIC_PERMISSIONS,
287
+ comment: str | None = None,
288
+ newline_replacement: NewlineReplacement = NewlineReplacement.LINEBREAK,
141
289
  ) -> RegionResource:
142
- if self.migration_metadata:
143
- raise InputError(
144
- f"The resource with the ID '{self.res_id}' already contains migration metadata, "
145
- f"no new data can be added."
290
+ """
291
+ Add several comments to the region
292
+
293
+ Args:
294
+ texts: list of texts
295
+ permissions: optional permissions of these values
296
+ comment: optional comment about these comments
297
+ newline_replacement: options how to deal with `\\n` inside the text value. Default: replace with `<br/>`
298
+
299
+ Returns:
300
+ The original region, with the added comments
301
+
302
+ Examples:
303
+ ```python
304
+ region = region.add_comment_multiple(["comment 1", "comment 2"])
305
+ ```
306
+ """
307
+ vals = check_and_fix_collection_input(texts, "hasComment", self.res_id)
308
+ comnts = [
309
+ Richtext.new(
310
+ value=x,
311
+ prop_name="hasComment",
312
+ permissions=permissions,
313
+ comment=comment,
314
+ resource_id=self.res_id,
315
+ newline_replacement=newline_replacement,
146
316
  )
147
- self.migration_metadata = MigrationMetadata(creation_date=creation_date, iri=iri, ark=ark, res_id=self.res_id)
148
- return self
149
-
150
- def serialise(self) -> etree._Element:
151
- self.comments = _transform_unexpected_input(self.comments, "comments", self.res_id)
152
- res_ele = self._serialise_resource_element()
153
- res_ele.append(self._serialise_geometry())
154
- res_ele.extend(self._serialise_values())
155
- if self.comments:
156
- res_ele.append(_serialise_has_comment(self.comments, self.res_id))
157
- return res_ele
158
-
159
- def _serialise_resource_element(self) -> etree._Element:
160
- attribs = {"label": self.label, "id": self.res_id}
161
- if self.permissions:
162
- attribs["permissions"] = self.permissions
163
- return etree.Element(f"{DASCH_SCHEMA}region", attrib=attribs, nsmap=XML_NAMESPACE_MAP)
164
-
165
- def _serialise_values(self) -> list[etree._Element]:
166
- return [
167
- ColorValue(value=self.color, prop_name="hasColor", resource_id=self.res_id).serialise(),
168
- LinkValue(value=self.region_of, prop_name="isRegionOf", resource_id=self.res_id).serialise(),
317
+ for x in vals
169
318
  ]
319
+ self.values.extend(comnts)
320
+ return self
170
321
 
171
- def _serialise_geometry(self) -> etree._Element:
172
- geo_prop = etree.Element(f"{DASCH_SCHEMA}geometry-prop", name="hasGeometry", nsmap=XML_NAMESPACE_MAP)
173
- ele = etree.Element(f"{DASCH_SCHEMA}geometry", nsmap=XML_NAMESPACE_MAP)
174
- ele.text = str(self.geometry)
175
- geo_prop.append(ele)
176
- return geo_prop
322
+ def add_comment_optional(
323
+ self,
324
+ text: Any,
325
+ permissions: Permissions = Permissions.PROJECT_SPECIFIC_PERMISSIONS,
326
+ comment: str | None = None,
327
+ newline_replacement: NewlineReplacement = NewlineReplacement.LINEBREAK,
328
+ ) -> RegionResource:
329
+ """
330
+ If the value is not empty, add it as comment, otherwise return the region unchanged.
331
+
332
+ Args:
333
+ text: text of the comment (or empty value)
334
+ permissions: optional permissions of this value
335
+ comment: optional comment about this comment
336
+ newline_replacement: options how to deal with `\\n` inside the text value. Default: replace with `<br/>`
337
+
338
+ Returns:
339
+ The original region, with the added comment
340
+
341
+ Examples:
342
+ ```python
343
+ region = region.add_comment_optional("comment text")
344
+ ```
345
+
346
+ ```python
347
+ region = region.add_comment_optional(None)
348
+ ```
349
+ """
350
+ if is_nonempty_value(text):
351
+ return self.add_comment(text, permissions, comment, newline_replacement)
352
+ return self
177
353
 
178
354
 
179
355
  @dataclass
180
356
  class LinkResource:
181
357
  res_id: str
182
358
  label: str
183
- link_to: list[str]
184
- comments: list[str]
185
- permissions: str = "res-default"
359
+ values: list[Value]
360
+ permissions: Permissions = Permissions.PROJECT_SPECIFIC_PERMISSIONS
186
361
  migration_metadata: MigrationMetadata | None = None
187
362
 
188
- def new(
189
- self, res_id: str, label: str, link_to: list[str], comments: list[str], permissions: str = "res-default"
363
+ @staticmethod
364
+ def create_new(
365
+ res_id: str,
366
+ label: str,
367
+ link_to: Collection[str],
368
+ permissions: Permissions = Permissions.PROJECT_SPECIFIC_PERMISSIONS,
190
369
  ) -> LinkResource:
370
+ """
371
+ Creates a new link resource.
372
+ A link resource is like a container that groups together several other resources of different classes.
373
+
374
+ [See XML documentation for details](https://docs.dasch.swiss/latest/DSP-TOOLS/file-formats/xml-data-file/#link)
375
+
376
+ Args:
377
+ res_id: ID of this link resource
378
+ label: label of this link resource
379
+ link_to: IDs of the resources that should be linked together (cardinality 1-n)
380
+ permissions: permissions of this link resource
381
+
382
+ Returns:
383
+ A link resource
384
+
385
+ Examples:
386
+ ```python
387
+ link_resource = xmllib.LinkResource.create_new(
388
+ res_id="ID",
389
+ label="label",
390
+ link_to=["target_resource_id_1", "target_resource_id_2"],
391
+ )
392
+ ```
393
+ """
394
+ _check_strings(string_to_check=res_id, res_id=res_id, field_name="Resource ID")
395
+ res_id = str(res_id)
396
+ lbl = check_and_fix_is_non_empty_string(value=label, res_id=res_id, value_field="label")
397
+ links_to = check_and_fix_collection_input(link_to, "hasLinkTo", res_id)
398
+ link_vals: list[Value] = [
399
+ LinkValue.new(value=x, prop_name="hasLinkTo", resource_id=res_id, comment=None, permissions=permissions)
400
+ for x in links_to
401
+ ]
191
402
  return LinkResource(
192
403
  res_id=res_id,
193
- label=label,
194
- link_to=link_to,
195
- comments=comments,
404
+ label=lbl,
405
+ values=link_vals,
196
406
  permissions=permissions,
197
407
  )
198
408
 
199
- def add_comment(self, comment: str) -> LinkResource:
200
- self.comments.append(comment)
409
+ def add_comment(
410
+ self,
411
+ text: str,
412
+ permissions: Permissions = Permissions.PROJECT_SPECIFIC_PERMISSIONS,
413
+ comment: str | None = None,
414
+ newline_replacement: NewlineReplacement = NewlineReplacement.LINEBREAK,
415
+ ) -> LinkResource:
416
+ """
417
+ Add a comment to the resource
418
+
419
+ Args:
420
+ text: text of the comment
421
+ permissions: optional permissions of this value
422
+ comment: optional comment about this comment
423
+ newline_replacement: options how to deal with `\\n` inside the text value. Default: replace with `<br/>`
424
+
425
+ Returns:
426
+ The original resource, with the added comment
427
+
428
+ Examples:
429
+ ```python
430
+ link_resource = link_resource.add_comment("comment text")
431
+ ```
432
+
433
+ ```python
434
+ link_resource = link_resource.add_comment(text="comment text", comment="Comment about the comment.")
435
+ ```
436
+ """
437
+ self.values.append(
438
+ Richtext.new(
439
+ value=text,
440
+ prop_name="hasComment",
441
+ permissions=permissions,
442
+ comment=comment,
443
+ resource_id=self.res_id,
444
+ newline_replacement=newline_replacement,
445
+ )
446
+ )
201
447
  return self
202
448
 
203
- def add_comments(self, comments: list[str]) -> LinkResource:
204
- self.comments.extend(comments)
449
+ def add_comment_multiple(
450
+ self,
451
+ texts: Collection[str],
452
+ permissions: Permissions = Permissions.PROJECT_SPECIFIC_PERMISSIONS,
453
+ comment: str | None = None,
454
+ newline_replacement: NewlineReplacement = NewlineReplacement.LINEBREAK,
455
+ ) -> LinkResource:
456
+ """
457
+ Add several comments to the resource
458
+
459
+ Args:
460
+ texts: list of texts
461
+ permissions: optional permissions of these values
462
+ comment: optional comment about this comment
463
+ newline_replacement: options how to deal with `\\n` inside the text value. Default: replace with `<br/>`
464
+
465
+ Returns:
466
+ The original resource, with the added comments
467
+
468
+ Examples:
469
+ ```python
470
+ link_resource = link_resource.add_comment_multiple(["comment 1", "comment 2"])
471
+ ```
472
+ """
473
+ vals = check_and_fix_collection_input(texts, "hasComment", self.res_id)
474
+ for v in vals:
475
+ self.add_comment(v, permissions, comment, newline_replacement)
205
476
  return self
206
477
 
207
- def add_migration_metadata(
208
- self, creation_date: str | None, iri: str | None = None, ark: str | None = None
478
+ def add_comment_optional(
479
+ self,
480
+ text: Any,
481
+ permissions: Permissions = Permissions.PROJECT_SPECIFIC_PERMISSIONS,
482
+ comment: str | None = None,
483
+ newline_replacement: NewlineReplacement = NewlineReplacement.LINEBREAK,
209
484
  ) -> LinkResource:
210
- if self.migration_metadata:
211
- raise InputError(
212
- f"The resource with the ID '{self.res_id}' already contains migration metadata, "
213
- f"no new data can be added."
214
- )
215
- self.migration_metadata = MigrationMetadata(creation_date=creation_date, iri=iri, ark=ark, res_id=self.res_id)
485
+ """
486
+ If the value is not empty, add it as comment, otherwise return the resource unchanged.
487
+
488
+ Args:
489
+ text: text of the comment (or empty value)
490
+ permissions: optional permissions of this value
491
+ comment: optional comment about this comment
492
+ newline_replacement: options how to deal with `\\n` inside the text value. Default: replace with `<br/>`
493
+
494
+ Returns:
495
+ The original resource, with the added comment
496
+
497
+ Examples:
498
+ ```python
499
+ link_resource = link_resource.add_comment_optional("comment text")
500
+ ```
501
+
502
+ ```python
503
+ link_resource = link_resource.add_comment_optional(None)
504
+ ```
505
+ """
506
+ if is_nonempty_value(text):
507
+ return self.add_comment(text, permissions, comment, newline_replacement)
216
508
  return self
217
509
 
218
- def serialise(self) -> etree._Element:
219
- self._check_for_and_convert_unexpected_input()
220
- res_ele = self._serialise_resource_element()
221
- res_ele.append(_serialise_has_comment(self.comments, self.res_id))
222
- res_ele.append(self._serialise_links())
223
- return res_ele
224
-
225
- def _check_for_and_convert_unexpected_input(self) -> None:
226
- _check_strings(string_to_check=self.res_id, res_id=self.res_id, field_name="Resource ID")
227
- _check_strings(string_to_check=self.label, res_id=self.res_id, field_name="Label")
228
- self.comments = _transform_unexpected_input(self.comments, "comments", self.res_id)
229
- self.link_to = _transform_unexpected_input(self.link_to, "link_to", self.res_id)
230
-
231
- def _serialise_resource_element(self) -> etree._Element:
232
- attribs = {"label": self.label, "id": self.res_id}
233
- if self.permissions:
234
- attribs["permissions"] = self.permissions
235
- return etree.Element(f"{DASCH_SCHEMA}link", attrib=attribs, nsmap=XML_NAMESPACE_MAP)
236
-
237
- def _serialise_links(self) -> etree._Element:
238
- prop_ele = etree.Element(f"{DASCH_SCHEMA}resptr-prop", name="hasLinkTo", nsmap=XML_NAMESPACE_MAP)
239
- vals = [LinkValue(value=x, prop_name="hasLinkTo", resource_id=self.res_id) for x in self.link_to]
240
- prop_ele.extend([v.make_element() for v in vals])
241
- return prop_ele
242
-
243
510
 
244
511
  @dataclass
245
512
  class SegmentBounds:
246
513
  segment_start: float | int | str
247
514
  segment_end: float | int | str
515
+ permissions: Permissions
248
516
  res_id: str
249
517
 
250
518
  def __post_init__(self) -> None:
@@ -254,309 +522,1021 @@ class SegmentBounds:
254
522
  if not is_decimal(self.segment_end):
255
523
  msg.append(f"Segment End Value: {self.segment_end} | Type: {type(self.segment_start)}")
256
524
  if msg:
257
- title = (
258
- f"The resource with the ID: '{self.res_id}' expects a float or integer for segment bounds. "
259
- f"The following places have an unexpected type:"
525
+ wrng = f"{LIST_SEPARATOR}{LIST_SEPARATOR.join(msg)}"
526
+ msg_info = MessageInfo(
527
+ f"Segment bounds must be a float or integer. The following places have an unexpected type: {wrng}",
528
+ self.res_id,
260
529
  )
261
- wrng = f"{title}{LIST_SEPARATOR}{LIST_SEPARATOR.join(msg)}"
262
- warnings.warn(DspToolsUserWarning(wrng))
530
+ emit_xmllib_input_warning(msg_info)
531
+ self.segment_start = str(self.segment_start)
532
+ self.segment_end = str(self.segment_end)
263
533
 
264
534
 
265
535
  @dataclass
266
536
  class VideoSegmentResource:
267
537
  res_id: str
268
538
  label: str
269
- segment_of: str
270
539
  segment_bounds: SegmentBounds
271
- title: str | None = None
272
- comments: list[str] = field(default_factory=list)
273
- descriptions: list[str] = field(default_factory=list)
274
- keywords: list[str] = field(default_factory=list)
275
- relates_to: list[str] = field(default_factory=list)
276
- permissions: str = "res-default"
540
+ values: list[Value]
541
+ permissions: Permissions = Permissions.PROJECT_SPECIFIC_PERMISSIONS
277
542
  migration_metadata: MigrationMetadata | None = None
278
543
 
279
- def new(
280
- self,
544
+ @staticmethod
545
+ def create_new(
281
546
  res_id: str,
282
547
  label: str,
283
548
  segment_of: str,
284
- segment_start: float,
285
- segment_end: float,
286
- title: str | None = None,
287
- permissions: str = "res-default",
549
+ segment_start: float | int | str,
550
+ segment_end: float | int | str,
551
+ permissions: Permissions = Permissions.PROJECT_SPECIFIC_PERMISSIONS,
288
552
  ) -> VideoSegmentResource:
553
+ """
554
+ Creates a new video segment resource, i.e. a time span of a MovingImageRepresentation.
555
+
556
+ [See XML documentation for details](https://docs.dasch.swiss/latest/DSP-TOOLS/file-formats/xml-data-file/#video-segment-and-audio-segment)
557
+
558
+ Args:
559
+ res_id: ID of this video segment resource
560
+ label: label of this video segment resource
561
+ segment_of: ID of the video resource that this segment refers to (cardinality 1)
562
+ segment_start: start of the segment in seconds (cardinality 1)
563
+ segment_end: end of the segment in seconds (cardinality 1)
564
+ permissions: permissions of this resource
565
+
566
+ Returns:
567
+ A video segment resource
568
+
569
+ Examples:
570
+ ```python
571
+ video_segment = xmllib.VideoSegmentResource.create_new(
572
+ res_id="ID",
573
+ label="label",
574
+ segment_of="video_resource_id",
575
+ segment_start=1.2,
576
+ segment_end=3.4,
577
+ )
578
+ ```
579
+ """
580
+ _check_strings(string_to_check=res_id, res_id=res_id, field_name="Resource ID")
581
+ res_id = str(res_id)
582
+ lbl = check_and_fix_is_non_empty_string(value=label, res_id=res_id, value_field="label")
583
+ segment_of_val = LinkValue.new(
584
+ value=segment_of, prop_name="isSegmentOf", permissions=permissions, comment=None, resource_id=res_id
585
+ )
289
586
  return VideoSegmentResource(
290
587
  res_id=res_id,
291
- label=label,
292
- segment_of=segment_of,
293
- segment_bounds=SegmentBounds(segment_start, segment_end, res_id),
294
- title=title,
588
+ label=lbl,
589
+ values=[segment_of_val],
590
+ segment_bounds=SegmentBounds(segment_start, segment_end, permissions, res_id),
295
591
  permissions=permissions,
296
592
  )
297
593
 
298
- def add_title(self, title: str) -> VideoSegmentResource:
299
- if self.title:
300
- _warn_value_exists(self.title, title, "title", self.res_id)
301
- self.title = title
594
+ def add_title(
595
+ self,
596
+ title: str,
597
+ permissions: Permissions = Permissions.PROJECT_SPECIFIC_PERMISSIONS,
598
+ comment: str | None = None,
599
+ ) -> VideoSegmentResource:
600
+ """
601
+ Add a title to the resource.
602
+
603
+ Args:
604
+ title: text
605
+ permissions: permissions of the value
606
+ comment: comments on the value
607
+
608
+ Returns:
609
+ The original resource, with the added title
610
+
611
+ Examples:
612
+ ```python
613
+ video_segment = video_segment.add_title("segment title")
614
+ ```
615
+ """
616
+ existing_titles = [x for x in self.values if x.prop_name == "hasTitle"]
617
+ if any(existing_titles):
618
+ _warn_value_exists(
619
+ old_value=existing_titles.pop(0).value, new_value=title, value_field="title", res_id=self.res_id
620
+ )
621
+ self.values.append(
622
+ SimpleText.new(
623
+ value=title,
624
+ prop_name="hasTitle",
625
+ permissions=permissions,
626
+ comment=comment,
627
+ resource_id=self.res_id,
628
+ )
629
+ )
302
630
  return self
303
631
 
304
- def add_comment(self, comment: str) -> VideoSegmentResource:
305
- self.comments.append(comment)
632
+ def add_title_optional(
633
+ self,
634
+ title: Any,
635
+ permissions: Permissions = Permissions.PROJECT_SPECIFIC_PERMISSIONS,
636
+ comment: str | None = None,
637
+ ) -> VideoSegmentResource:
638
+ """
639
+ If the value is not empty, add it as title, otherwise return the resource unchanged.
640
+
641
+ Args:
642
+ title: text or empty value
643
+ permissions: permissions of the value
644
+ comment: comments on the value
645
+
646
+ Returns:
647
+ The original resource, with the added title
648
+
649
+ Examples:
650
+ ```python
651
+ video_segment = video_segment.add_title("segment title")
652
+ ```
653
+
654
+ ```python
655
+ video_segment = video_segment.add_title(None)
656
+ ```
657
+ """
658
+ if is_nonempty_value(title):
659
+ self.add_title(title, permissions, comment)
306
660
  return self
307
661
 
308
- def add_comments(self, comments: list[str]) -> VideoSegmentResource:
309
- self.comments.extend(comments)
662
+ def add_comment(
663
+ self,
664
+ text: str,
665
+ permissions: Permissions = Permissions.PROJECT_SPECIFIC_PERMISSIONS,
666
+ comment: str | None = None,
667
+ newline_replacement: NewlineReplacement = NewlineReplacement.LINEBREAK,
668
+ ) -> VideoSegmentResource:
669
+ """
670
+ Add a comment to the resource
671
+
672
+ Args:
673
+ text: text
674
+ permissions: optional permissions of this value
675
+ comment: optional comment about this comment
676
+ newline_replacement: options how to deal with `\\n` inside the text value. Default: replace with `<br/>`
677
+
678
+ Returns:
679
+ The original resource, with the added comment
680
+
681
+ Examples:
682
+ ```python
683
+ video_segment = video_segment.add_comment("comment text")
684
+ ```
685
+ """
686
+ self.values.append(
687
+ Richtext.new(
688
+ value=text,
689
+ prop_name="hasComment",
690
+ permissions=permissions,
691
+ comment=comment,
692
+ resource_id=self.res_id,
693
+ newline_replacement=newline_replacement,
694
+ )
695
+ )
310
696
  return self
311
697
 
312
- def add_description(self, description: str) -> VideoSegmentResource:
313
- self.descriptions.append(description)
698
+ def add_comment_multiple(
699
+ self,
700
+ texts: Collection[str],
701
+ permissions: Permissions = Permissions.PROJECT_SPECIFIC_PERMISSIONS,
702
+ comment: str | None = None,
703
+ newline_replacement: NewlineReplacement = NewlineReplacement.LINEBREAK,
704
+ ) -> VideoSegmentResource:
705
+ """
706
+ Add several comments to the resource
707
+
708
+ Args:
709
+ texts: list of texts
710
+ permissions: optional permissions of these values
711
+ comment: optional comment about these comments
712
+ newline_replacement: options how to deal with `\\n` inside the text value. Default: replace with `<br/>`
713
+
714
+ Returns:
715
+ The original resource, with the added comments
716
+
717
+ Examples:
718
+ ```python
719
+ video_segment = video_segment.add_comment_multiple(["comment 1", "comment 2"])
720
+ ```
721
+ """
722
+ vals = check_and_fix_collection_input(texts, "hasComment", self.res_id)
723
+ for v in vals:
724
+ self.add_comment(v, permissions, comment, newline_replacement)
314
725
  return self
315
726
 
316
- def add_descriptions(self, descriptions: list[str]) -> VideoSegmentResource:
317
- self.descriptions.extend(descriptions)
727
+ def add_comment_optional(
728
+ self,
729
+ text: Any,
730
+ permissions: Permissions = Permissions.PROJECT_SPECIFIC_PERMISSIONS,
731
+ comment: str | None = None,
732
+ newline_replacement: NewlineReplacement = NewlineReplacement.LINEBREAK,
733
+ ) -> VideoSegmentResource:
734
+ """
735
+ If the value is not empty, add it as comment, otherwise return the resource unchanged.
736
+
737
+ Args:
738
+ text: text of the comment (or empty value)
739
+ permissions: optional permissions of this value
740
+ comment: optional comment about this comment
741
+ newline_replacement: options how to deal with `\\n` inside the text value. Default: replace with `<br/>`
742
+
743
+ Returns:
744
+ The original resource, with the added comment
745
+
746
+ Examples:
747
+ ```python
748
+ video_segment = video_segment.add_comment_optional("comment text")
749
+ ```
750
+
751
+ ```python
752
+ video_segment = video_segment.add_comment_optional(None)
753
+ ```
754
+ """
755
+ if is_nonempty_value(text):
756
+ self.add_comment(text, permissions, comment, newline_replacement)
318
757
  return self
319
758
 
320
- def add_keyword(self, keywords: str) -> VideoSegmentResource:
321
- self.keywords.append(keywords)
759
+ def add_description(
760
+ self,
761
+ description: str,
762
+ permissions: Permissions = Permissions.PROJECT_SPECIFIC_PERMISSIONS,
763
+ comment: str | None = None,
764
+ newline_replacement: NewlineReplacement = NewlineReplacement.LINEBREAK,
765
+ ) -> VideoSegmentResource:
766
+ """
767
+ Add a description to the resource
768
+
769
+ Args:
770
+ description: text
771
+ permissions: optional permissions of this value
772
+ comment: optional comment
773
+ newline_replacement: options how to deal with `\\n` inside the text value. Default: replace with `<br/>`
774
+
775
+ Returns:
776
+ The original resource, with the added description
777
+
778
+ Examples:
779
+ ```python
780
+ video_segment = video_segment.add_description("description text")
781
+ ```
782
+ """
783
+ self.values.append(
784
+ Richtext.new(
785
+ value=description,
786
+ prop_name="hasDescription",
787
+ permissions=permissions,
788
+ comment=comment,
789
+ resource_id=self.res_id,
790
+ newline_replacement=newline_replacement,
791
+ )
792
+ )
322
793
  return self
323
794
 
324
- def add_keywords(self, keywords: list[str]) -> VideoSegmentResource:
325
- self.keywords.extend(keywords)
795
+ def add_description_multiple(
796
+ self,
797
+ descriptions: Collection[str],
798
+ permissions: Permissions = Permissions.PROJECT_SPECIFIC_PERMISSIONS,
799
+ comment: str | None = None,
800
+ newline_replacement: NewlineReplacement = NewlineReplacement.LINEBREAK,
801
+ ) -> VideoSegmentResource:
802
+ """
803
+ Add several descriptions to the resource
804
+
805
+ Args:
806
+ descriptions: list of texts
807
+ permissions: optional permissions of these value
808
+ comment: optional comment
809
+ newline_replacement: options how to deal with `\\n` inside the text value. Default: replace with `<br/>`
810
+
811
+ Returns:
812
+ The original resource, with the added descriptions
813
+
814
+ Examples:
815
+ ```python
816
+ video_segment = video_segment.add_description_multiple(["description 1", "description 2"])
817
+ ```
818
+ """
819
+ vals = check_and_fix_collection_input(descriptions, "description", self.res_id)
820
+ for v in vals:
821
+ self.add_description(v, permissions, comment, newline_replacement)
326
822
  return self
327
823
 
328
- def add_relates_to(self, relates_to: str) -> VideoSegmentResource:
329
- self.relates_to.append(relates_to)
824
+ def add_description_optional(
825
+ self,
826
+ description: Any,
827
+ permissions: Permissions = Permissions.PROJECT_SPECIFIC_PERMISSIONS,
828
+ comment: str | None = None,
829
+ newline_replacement: NewlineReplacement = NewlineReplacement.LINEBREAK,
830
+ ) -> VideoSegmentResource:
831
+ """
832
+ If the value is not empty, add it as description, otherwise return the resource unchanged.
833
+
834
+ Args:
835
+ description: text or empty value
836
+ permissions: optional permissions of this value
837
+ comment: optional comment
838
+ newline_replacement: options how to deal with `\\n` inside the text value. Default: replace with `<br/>`
839
+
840
+ Returns:
841
+ The original resource, with the added description
842
+
843
+ Examples:
844
+ ```python
845
+ video_segment = video_segment.add_description_optional("description text")
846
+ ```
847
+
848
+ ```python
849
+ video_segment = video_segment.add_description_optional(None)
850
+ ```
851
+ """
852
+ if is_nonempty_value(description):
853
+ self.add_description(description, permissions, comment, newline_replacement)
330
854
  return self
331
855
 
332
- def add_relates_to_multiple(self, relates_to: list[str]) -> VideoSegmentResource:
333
- self.relates_to.extend(relates_to)
856
+ def add_keyword(
857
+ self,
858
+ keyword: str,
859
+ permissions: Permissions = Permissions.PROJECT_SPECIFIC_PERMISSIONS,
860
+ comment: str | None = None,
861
+ ) -> VideoSegmentResource:
862
+ """
863
+ Add a keyword to the resource
864
+
865
+ Args:
866
+ keyword: text
867
+ permissions: optional permissions of this value
868
+ comment: optional comment
869
+
870
+ Returns:
871
+ The original resource, with the added keyword
872
+
873
+ Examples:
874
+ ```python
875
+ video_segment = video_segment.add_keyword("keyword")
876
+ ```
877
+ """
878
+ self.values.append(
879
+ SimpleText.new(
880
+ value=keyword,
881
+ prop_name="hasKeyword",
882
+ permissions=permissions,
883
+ comment=comment,
884
+ resource_id=self.res_id,
885
+ )
886
+ )
334
887
  return self
335
888
 
336
- def add_migration_metadata(
337
- self, creation_date: str | None, iri: str | None = None, ark: str | None = None
889
+ def add_keyword_multiple(
890
+ self,
891
+ keywords: Collection[str],
892
+ permissions: Permissions = Permissions.PROJECT_SPECIFIC_PERMISSIONS,
893
+ comment: str | None = None,
338
894
  ) -> VideoSegmentResource:
339
- if self.migration_metadata:
340
- raise InputError(
341
- f"The resource with the ID '{self.res_id}' already contains migration metadata, "
342
- f"no new data can be added."
343
- )
344
- self.migration_metadata = MigrationMetadata(creation_date=creation_date, iri=iri, ark=ark, res_id=self.res_id)
895
+ """
896
+ Add several keywords to the resource
897
+
898
+ Args:
899
+ keywords: list of texts
900
+ permissions: optional permissions of these values
901
+ comment: optional comment
902
+
903
+ Returns:
904
+ The original resource, with the added keywords
905
+
906
+ Examples:
907
+ ```python
908
+ video_segment = video_segment.add_keyword_multiple(["keyword 1", "keyword 2"])
909
+ ```
910
+ """
911
+ vals = check_and_fix_collection_input(keywords, "keywords", self.res_id)
912
+ for v in vals:
913
+ self.add_keyword(v, permissions, comment)
345
914
  return self
346
915
 
347
- def serialise(self) -> etree._Element:
348
- self._check_for_and_convert_unexpected_input()
349
- res_ele = self._serialise_resource_element()
350
- res_ele.extend(_serialise_segment_children(self))
351
- return res_ele
916
+ def add_keyword_optional(
917
+ self,
918
+ keyword: Any,
919
+ permissions: Permissions = Permissions.PROJECT_SPECIFIC_PERMISSIONS,
920
+ comment: str | None = None,
921
+ ) -> VideoSegmentResource:
922
+ """
923
+ If the value is not empty, add it as keyword, otherwise return the resource unchanged.
924
+
925
+ Args:
926
+ keyword: text or empty value
927
+ permissions: optional permissions of this value
928
+ comment: optional comment
929
+
930
+ Returns:
931
+ The original resource, with the added keyword
932
+
933
+ Examples:
934
+ ```python
935
+ video_segment = video_segment.add_keyword_optional("keyword")
936
+ ```
937
+
938
+ ```python
939
+ video_segment = video_segment.add_keyword_optional(None)
940
+ ```
941
+ """
942
+ if is_nonempty_value(keyword):
943
+ self.add_keyword(keyword, permissions, comment)
944
+ return self
352
945
 
353
- def _check_for_and_convert_unexpected_input(self) -> None:
354
- self.comments = _transform_unexpected_input(self.comments, "comments", self.res_id)
355
- self.descriptions = _transform_unexpected_input(self.descriptions, "descriptions", self.res_id)
356
- self.keywords = _transform_unexpected_input(self.keywords, "keywords", self.res_id)
357
- self.relates_to = _transform_unexpected_input(self.relates_to, "relates_to", self.res_id)
358
- _validate_segment(self)
946
+ def add_relates_to(
947
+ self,
948
+ relates_to: str,
949
+ permissions: Permissions = Permissions.PROJECT_SPECIFIC_PERMISSIONS,
950
+ comment: str | None = None,
951
+ ) -> VideoSegmentResource:
952
+ """
953
+ Add a link to a related resource
954
+
955
+ Args:
956
+ relates_to: ID of the related resource
957
+ permissions: optional permissions of this value
958
+ comment: optional comment
959
+
960
+ Returns:
961
+ The original resource, with the added related resource
962
+
963
+ Examples:
964
+ ```python
965
+ video_segment = video_segment.add_relates_to("target_resource_id")
966
+ ```
967
+ """
968
+ self.values.append(
969
+ LinkValue.new(
970
+ value=relates_to,
971
+ prop_name="relatesTo",
972
+ permissions=permissions,
973
+ comment=comment,
974
+ resource_id=self.res_id,
975
+ )
976
+ )
977
+ return self
978
+
979
+ def add_relates_to_multiple(
980
+ self,
981
+ relates_to: Collection[str],
982
+ permissions: Permissions = Permissions.PROJECT_SPECIFIC_PERMISSIONS,
983
+ comment: str | None = None,
984
+ ) -> VideoSegmentResource:
985
+ """
986
+ Add several links to related resources
987
+
988
+ Args:
989
+ relates_to: list of IDs of the related resources
990
+ permissions: optional permissions of these values
991
+ comment: optional comment
992
+
993
+ Returns:
994
+ The original resource, with the added related resources
995
+
996
+ Examples:
997
+ ```python
998
+ video_segment = video_segment.add_relates_to_multiple(["target_resource_id_1", "target_resource_id_2"])
999
+ ```
1000
+ """
1001
+ vals = check_and_fix_collection_input(relates_to, "relatesTo", self.res_id)
1002
+ for v in vals:
1003
+ self.add_relates_to(v, permissions, comment)
1004
+ return self
359
1005
 
360
- def _serialise_resource_element(self) -> etree._Element:
361
- attribs = {"label": self.label, "id": self.res_id}
362
- if self.permissions:
363
- attribs["permissions"] = self.permissions
364
- return etree.Element(f"{DASCH_SCHEMA}video-segment", attrib=attribs, nsmap=XML_NAMESPACE_MAP)
1006
+ def add_relates_to_optional(
1007
+ self,
1008
+ relates_to: Any,
1009
+ permissions: Permissions = Permissions.PROJECT_SPECIFIC_PERMISSIONS,
1010
+ comment: str | None = None,
1011
+ ) -> VideoSegmentResource:
1012
+ """
1013
+ If the value is not empty, add it as related resource, otherwise return the resource unchanged.
1014
+
1015
+ Args:
1016
+ relates_to: ID or the related resource or empty value
1017
+ permissions: optional permissions of this value
1018
+ comment: optional comment
1019
+
1020
+ Returns:
1021
+ The original resource, with the added related resources
1022
+
1023
+ Examples:
1024
+ ```python
1025
+ video_segment = video_segment.add_relates_to_optional("target_resource_id")
1026
+ ```
1027
+
1028
+ ```python
1029
+ video_segment = video_segment.add_relates_to_optional(None)
1030
+ ```
1031
+ """
1032
+ if is_nonempty_value(relates_to):
1033
+ self.add_relates_to(relates_to, permissions, comment)
1034
+ return self
365
1035
 
366
1036
 
367
1037
  @dataclass
368
1038
  class AudioSegmentResource:
369
1039
  res_id: str
370
1040
  label: str
371
- segment_of: str
372
1041
  segment_bounds: SegmentBounds
373
- title: str | None = None
374
- comments: list[str] = field(default_factory=list)
375
- descriptions: list[str] = field(default_factory=list)
376
- keywords: list[str] = field(default_factory=list)
377
- relates_to: list[str] = field(default_factory=list)
378
- permissions: str = "res-default"
1042
+ values: list[Value]
1043
+ permissions: Permissions = Permissions.PROJECT_SPECIFIC_PERMISSIONS
379
1044
  migration_metadata: MigrationMetadata | None = None
380
1045
 
381
- def new(
382
- self,
1046
+ @staticmethod
1047
+ def create_new(
383
1048
  res_id: str,
384
1049
  label: str,
385
1050
  segment_of: str,
386
- segment_start: float,
387
- segment_end: float,
388
- title: str | None = None,
389
- permissions: str = "res-default",
1051
+ segment_start: float | int | str,
1052
+ segment_end: float | int | str,
1053
+ permissions: Permissions = Permissions.PROJECT_SPECIFIC_PERMISSIONS,
390
1054
  ) -> AudioSegmentResource:
1055
+ """
1056
+ Creates a new audio segment resource, i.e. a time span of an AudioRepresentation.
1057
+
1058
+ [See XML documentation for details](https://docs.dasch.swiss/latest/DSP-TOOLS/file-formats/xml-data-file/#video-segment-and-audio-segment)
1059
+
1060
+ Args:
1061
+ res_id: ID of this audio segment resource
1062
+ label: label of this audio segment resource
1063
+ segment_of: ID of the audio resource that this segment refers to (cardinality 1)
1064
+ segment_start: start of the segment in seconds (cardinality 1)
1065
+ segment_end: end of the segment in seconds (cardinality 1)
1066
+ permissions: permissions of this resource
1067
+
1068
+ Returns:
1069
+ An audio segment resource
1070
+ """
1071
+ _check_strings(string_to_check=res_id, res_id=res_id, field_name="Resource ID")
1072
+ res_id = str(res_id)
1073
+ lbl = check_and_fix_is_non_empty_string(value=label, res_id=res_id, value_field="label")
1074
+ segment_of_val = LinkValue.new(
1075
+ value=segment_of, prop_name="isSegmentOf", permissions=permissions, comment=None, resource_id=res_id
1076
+ )
391
1077
  return AudioSegmentResource(
392
1078
  res_id=res_id,
393
- label=label,
394
- segment_of=segment_of,
395
- segment_bounds=SegmentBounds(segment_start, segment_end, res_id),
396
- title=title,
1079
+ label=lbl,
1080
+ segment_bounds=SegmentBounds(segment_start, segment_end, permissions, res_id),
1081
+ values=[segment_of_val],
397
1082
  permissions=permissions,
398
1083
  )
399
1084
 
400
- def add_title(self, title: str) -> AudioSegmentResource:
401
- if self.title:
402
- _warn_value_exists(self.title, title, "title", self.res_id)
403
- self.title = title
1085
+ def add_title(
1086
+ self,
1087
+ title: str,
1088
+ permissions: Permissions = Permissions.PROJECT_SPECIFIC_PERMISSIONS,
1089
+ comment: str | None = None,
1090
+ ) -> AudioSegmentResource:
1091
+ """
1092
+ Add a title to the resource.
1093
+
1094
+ Args:
1095
+ title: text
1096
+ permissions: permissions of the value
1097
+ comment: comments on the value
1098
+
1099
+ Returns:
1100
+ The original resource, with the added title
1101
+
1102
+ Examples:
1103
+ ```python
1104
+ audio_segment = audio_segment.add_title("segment title")
1105
+ ```
1106
+ """
1107
+ existing_title = [x for x in self.values if x.prop_name == "hasTitle"]
1108
+ if any(existing_title):
1109
+ _warn_value_exists(
1110
+ old_value=existing_title.pop(0).value, new_value=title, value_field="title", res_id=self.res_id
1111
+ )
1112
+ self.values.append(
1113
+ SimpleText.new(
1114
+ value=title,
1115
+ prop_name="hasTitle",
1116
+ permissions=permissions,
1117
+ comment=comment,
1118
+ resource_id=self.res_id,
1119
+ )
1120
+ )
1121
+ return self
1122
+
1123
+ def add_title_optional(
1124
+ self,
1125
+ title: Any,
1126
+ permissions: Permissions = Permissions.PROJECT_SPECIFIC_PERMISSIONS,
1127
+ comment: str | None = None,
1128
+ ) -> AudioSegmentResource:
1129
+ """
1130
+ If the value is not empty, add it as title, otherwise return the resource unchanged.
1131
+
1132
+ Args:
1133
+ title: text or empty value
1134
+ permissions: permissions of the value
1135
+ comment: comments on the value
1136
+
1137
+ Returns:
1138
+ The original resource, with the added title
1139
+
1140
+ Examples:
1141
+ ```python
1142
+ audio_segment = audio_segment.add_title("segment title")
1143
+ ```
1144
+
1145
+ ```python
1146
+ audio_segment = audio_segment.add_title(None)
1147
+ ```
1148
+ """
1149
+ if is_nonempty_value(title):
1150
+ self.add_title(title, permissions, comment)
1151
+ return self
1152
+
1153
+ def add_comment(
1154
+ self,
1155
+ text: str,
1156
+ permissions: Permissions = Permissions.PROJECT_SPECIFIC_PERMISSIONS,
1157
+ comment: str | None = None,
1158
+ newline_replacement: NewlineReplacement = NewlineReplacement.LINEBREAK,
1159
+ ) -> AudioSegmentResource:
1160
+ """
1161
+ Add a comment to the resource
1162
+
1163
+ Args:
1164
+ text: text of the comment
1165
+ permissions: optional permissions of this value
1166
+ comment: optional comment about this comment
1167
+ newline_replacement: options how to deal with `\\n` inside the text value. Default: replace with `<br/>`
1168
+
1169
+ Returns:
1170
+ The original resource, with the added comment
1171
+
1172
+ Examples:
1173
+ ```python
1174
+ audio_segment = audio_segment.add_comment("comment text")
1175
+ ```
1176
+ """
1177
+ self.values.append(
1178
+ Richtext.new(
1179
+ value=text,
1180
+ prop_name="hasComment",
1181
+ permissions=permissions,
1182
+ comment=comment,
1183
+ resource_id=self.res_id,
1184
+ newline_replacement=newline_replacement,
1185
+ )
1186
+ )
404
1187
  return self
405
1188
 
406
- def add_comment(self, comment: str) -> AudioSegmentResource:
407
- self.comments.append(comment)
1189
+ def add_comment_multiple(
1190
+ self,
1191
+ texts: Collection[str],
1192
+ permissions: Permissions = Permissions.PROJECT_SPECIFIC_PERMISSIONS,
1193
+ comment: str | None = None,
1194
+ newline_replacement: NewlineReplacement = NewlineReplacement.LINEBREAK,
1195
+ ) -> AudioSegmentResource:
1196
+ """
1197
+ Add several comments to the resource
1198
+
1199
+ Args:
1200
+ texts: list of texts
1201
+ permissions: optional permissions of these values
1202
+ comment: optional comment about these comments
1203
+ newline_replacement: options how to deal with `\\n` inside the text value. Default: replace with `<br/>`
1204
+
1205
+ Returns:
1206
+ The original resource, with the added comments
1207
+
1208
+ Examples:
1209
+ ```python
1210
+ audio_segment = audio_segment.add_comment_multiple(["comment 1", "comment 2"])
1211
+ ```
1212
+ """
1213
+ vals = check_and_fix_collection_input(texts, "hasComment", self.res_id)
1214
+ for v in vals:
1215
+ self.add_comment(v, permissions, comment, newline_replacement)
408
1216
  return self
409
1217
 
410
- def add_comments(self, comments: list[str]) -> AudioSegmentResource:
411
- self.comments.extend(comments)
1218
+ def add_comment_optional(
1219
+ self,
1220
+ text: Any,
1221
+ permissions: Permissions = Permissions.PROJECT_SPECIFIC_PERMISSIONS,
1222
+ comment: str | None = None,
1223
+ newline_replacement: NewlineReplacement = NewlineReplacement.LINEBREAK,
1224
+ ) -> AudioSegmentResource:
1225
+ """
1226
+ If the value is not empty, add it as comment, otherwise return the resource unchanged.
1227
+
1228
+ Args:
1229
+ text: text of the comment (or empty value)
1230
+ permissions: optional permissions of this value
1231
+ comment: optional comment about this comment
1232
+ newline_replacement: options how to deal with `\\n` inside the text value. Default: replace with `<br/>`
1233
+
1234
+ Returns:
1235
+ The original resource, with the added comment
1236
+
1237
+ Examples:
1238
+ ```python
1239
+ audio_segment = audio_segment.add_comment_optional("comment text")
1240
+ ```
1241
+
1242
+ ```python
1243
+ audio_segment = audio_segment.add_comment_optional(None)
1244
+ ```
1245
+ """
1246
+ if is_nonempty_value(text):
1247
+ self.add_comment(text, permissions, comment, newline_replacement)
412
1248
  return self
413
1249
 
414
- def add_description(self, description: str) -> AudioSegmentResource:
415
- self.descriptions.append(description)
1250
+ def add_description(
1251
+ self,
1252
+ description: str,
1253
+ permissions: Permissions = Permissions.PROJECT_SPECIFIC_PERMISSIONS,
1254
+ comment: str | None = None,
1255
+ newline_replacement: NewlineReplacement = NewlineReplacement.LINEBREAK,
1256
+ ) -> AudioSegmentResource:
1257
+ """
1258
+ Add a description to the resource
1259
+
1260
+ Args:
1261
+ description: text
1262
+ permissions: optional permissions of this value
1263
+ comment: optional comment
1264
+ newline_replacement: options how to deal with `\\n` inside the text value. Default: replace with `<br/>`
1265
+
1266
+ Returns:
1267
+ The original resource, with the added description
1268
+
1269
+ Examples:
1270
+ ```python
1271
+ audio_segment = audio_segment.add_description("description text")
1272
+ ```
1273
+ """
1274
+ self.values.append(
1275
+ Richtext.new(
1276
+ value=description,
1277
+ prop_name="hasDescription",
1278
+ permissions=permissions,
1279
+ comment=comment,
1280
+ resource_id=self.res_id,
1281
+ newline_replacement=newline_replacement,
1282
+ )
1283
+ )
416
1284
  return self
417
1285
 
418
- def add_descriptions(self, descriptions: list[str]) -> AudioSegmentResource:
419
- self.descriptions.extend(descriptions)
1286
+ def add_description_multiple(
1287
+ self,
1288
+ descriptions: Collection[str],
1289
+ permissions: Permissions = Permissions.PROJECT_SPECIFIC_PERMISSIONS,
1290
+ comment: str | None = None,
1291
+ newline_replacement: NewlineReplacement = NewlineReplacement.LINEBREAK,
1292
+ ) -> AudioSegmentResource:
1293
+ """
1294
+ Add several descriptions to the resource
1295
+
1296
+ Args:
1297
+ descriptions: list of texts
1298
+ permissions: optional permissions of these values
1299
+ comment: optional comment
1300
+ newline_replacement: options how to deal with `\\n` inside the text value. Default: replace with `<br/>`
1301
+
1302
+ Returns:
1303
+ The original resource, with the added descriptions
1304
+
1305
+ Examples:
1306
+ ```python
1307
+ audio_segment = audio_segment.add_description_multiple(["description 1", "description 2"])
1308
+ ```
1309
+ """
1310
+ vals = check_and_fix_collection_input(descriptions, "description", self.res_id)
1311
+ for v in vals:
1312
+ self.add_description(v, permissions, comment, newline_replacement)
420
1313
  return self
421
1314
 
422
- def add_keyword(self, keywords: str) -> AudioSegmentResource:
423
- self.keywords.append(keywords)
1315
+ def add_description_optional(
1316
+ self,
1317
+ description: Any,
1318
+ permissions: Permissions = Permissions.PROJECT_SPECIFIC_PERMISSIONS,
1319
+ comment: str | None = None,
1320
+ newline_replacement: NewlineReplacement = NewlineReplacement.LINEBREAK,
1321
+ ) -> AudioSegmentResource:
1322
+ """
1323
+ If the value is not empty, add it as description, otherwise return the resource unchanged.
1324
+
1325
+ Args:
1326
+ description: text or empty value
1327
+ permissions: optional permissions of this value
1328
+ comment: optional comment
1329
+ newline_replacement: options how to deal with `\\n` inside the text value. Default: replace with `<br/>`
1330
+
1331
+ Returns:
1332
+ The original resource, with the added description
1333
+
1334
+ Examples:
1335
+ ```python
1336
+ audio_segment = audio_segment.add_description_optional("description text")
1337
+ ```
1338
+
1339
+ ```python
1340
+ audio_segment = audio_segment.add_description_optional(None)
1341
+ ```
1342
+ """
1343
+ if is_nonempty_value(description):
1344
+ self.add_description(description, permissions, comment, newline_replacement)
424
1345
  return self
425
1346
 
426
- def add_keywords(self, keywords: list[str]) -> AudioSegmentResource:
427
- self.keywords.extend(keywords)
1347
+ def add_keyword(
1348
+ self,
1349
+ keyword: str,
1350
+ permissions: Permissions = Permissions.PROJECT_SPECIFIC_PERMISSIONS,
1351
+ comment: str | None = None,
1352
+ ) -> AudioSegmentResource:
1353
+ """
1354
+ Add a keyword to the resource
1355
+
1356
+ Args:
1357
+ keyword: text
1358
+ permissions: optional permissions of this value
1359
+ comment: optional comment
1360
+
1361
+ Returns:
1362
+ The original resource, with the added keyword
1363
+
1364
+ Examples:
1365
+ ```python
1366
+ audio_segment = audio_segment.add_keyword("keyword")
1367
+ ```
1368
+ """
1369
+ self.values.append(
1370
+ SimpleText.new(
1371
+ value=keyword,
1372
+ prop_name="hasKeyword",
1373
+ permissions=permissions,
1374
+ comment=comment,
1375
+ resource_id=self.res_id,
1376
+ )
1377
+ )
428
1378
  return self
429
1379
 
430
- def add_relates_to(self, relates_to: str) -> AudioSegmentResource:
431
- self.relates_to.append(relates_to)
1380
+ def add_keyword_multiple(
1381
+ self,
1382
+ keywords: Collection[str],
1383
+ permissions: Permissions = Permissions.PROJECT_SPECIFIC_PERMISSIONS,
1384
+ comment: str | None = None,
1385
+ ) -> AudioSegmentResource:
1386
+ """
1387
+ Add several keywords to the resource
1388
+
1389
+ Args:
1390
+ keywords: list of texts
1391
+ permissions: optional permissions of these values
1392
+ comment: optional comment
1393
+
1394
+ Returns:
1395
+ The original resource, with the added keywords
1396
+
1397
+ Examples:
1398
+ ```python
1399
+ audio_segment = audio_segment.add_keyword_multiple(["keyword 1", "keyword 2"])
1400
+ ```
1401
+ """
1402
+ vals = check_and_fix_collection_input(keywords, "keywords", self.res_id)
1403
+ for v in vals:
1404
+ self.add_keyword(v, permissions, comment)
432
1405
  return self
433
1406
 
434
- def add_relates_to_multiple(self, relates_to: list[str]) -> AudioSegmentResource:
435
- self.relates_to.extend(relates_to)
1407
+ def add_keyword_optional(
1408
+ self,
1409
+ keyword: Any,
1410
+ permissions: Permissions = Permissions.PROJECT_SPECIFIC_PERMISSIONS,
1411
+ comment: str | None = None,
1412
+ ) -> AudioSegmentResource:
1413
+ """
1414
+ If the value is not empty, add it as keyword, otherwise return the resource unchanged.
1415
+
1416
+ Args:
1417
+ keyword: text or empty value
1418
+ permissions: optional permissions of this value
1419
+ comment: optional comment
1420
+
1421
+ Returns:
1422
+ The original resource, with the added keyword
1423
+
1424
+ Examples:
1425
+ ```python
1426
+ audio_segment = audio_segment.add_keyword_optional("keyword")
1427
+ ```
1428
+
1429
+ ```python
1430
+ audio_segment = audio_segment.add_keyword_optional(None)
1431
+ ```
1432
+ """
1433
+ if is_nonempty_value(keyword):
1434
+ self.add_keyword(keyword, permissions, comment)
436
1435
  return self
437
1436
 
438
- def add_migration_metadata(
439
- self, creation_date: str | None, iri: str | None = None, ark: str | None = None
1437
+ def add_relates_to(
1438
+ self,
1439
+ relates_to: str,
1440
+ permissions: Permissions = Permissions.PROJECT_SPECIFIC_PERMISSIONS,
1441
+ comment: str | None = None,
440
1442
  ) -> AudioSegmentResource:
441
- if self.migration_metadata:
442
- raise InputError(
443
- f"The resource with the ID '{self.res_id}' already contains migration metadata, "
444
- f"no new data can be added."
1443
+ """
1444
+ Add a link to a related resource
1445
+
1446
+ Args:
1447
+ relates_to: ID of the related resource
1448
+ permissions: optional permissions of this value
1449
+ comment: optional comment
1450
+
1451
+ Returns:
1452
+ The original resource, with the added related resource
1453
+
1454
+ Examples:
1455
+ ```python
1456
+ audio_segment = audio_segment.add_relates_to("target_resource_id")
1457
+ ```
1458
+ """
1459
+ self.values.append(
1460
+ LinkValue.new(
1461
+ value=relates_to,
1462
+ prop_name="relatesTo",
1463
+ permissions=permissions,
1464
+ comment=comment,
1465
+ resource_id=self.res_id,
445
1466
  )
446
- self.migration_metadata = MigrationMetadata(creation_date=creation_date, iri=iri, ark=ark, res_id=self.res_id)
447
- return self
448
-
449
- def serialise(self) -> etree._Element:
450
- self._check_for_and_convert_unexpected_input()
451
- res_ele = self._serialise_resource_element()
452
- res_ele.extend(_serialise_segment_children(self))
453
- return res_ele
454
-
455
- def _check_for_and_convert_unexpected_input(self) -> None:
456
- self.comments = _transform_unexpected_input(self.comments, "comments", self.res_id)
457
- self.descriptions = _transform_unexpected_input(self.descriptions, "descriptions", self.res_id)
458
- self.keywords = _transform_unexpected_input(self.keywords, "keywords", self.res_id)
459
- self.relates_to = _transform_unexpected_input(self.relates_to, "relates_to", self.res_id)
460
- _validate_segment(self)
461
-
462
- def _serialise_resource_element(self) -> etree._Element:
463
- attribs = {"label": self.label, "id": self.res_id}
464
- if self.permissions:
465
- attribs["permissions"] = self.permissions
466
- return etree.Element(f"{DASCH_SCHEMA}audio-segment", attrib=attribs, nsmap=XML_NAMESPACE_MAP)
467
-
468
-
469
- def _check_strings(string_to_check: str, res_id: str, field_name: str) -> None:
470
- if not is_string_like(string_to_check):
471
- msg = (
472
- f"The resource with the ID '{res_id}' has an invalid string at the following location:\n"
473
- f"Field: {field_name} | Value: {string_to_check}"
474
1467
  )
475
- warnings.warn(DspToolsUserWarning(msg))
476
-
477
-
478
- def _serialise_has_comment(comments: list[str], res_id: str) -> etree._Element:
479
- cmts = [Richtext(value=x, prop_name="hasComment", resource_id=res_id) for x in comments]
480
- cmt_prop = cmts[0].make_prop()
481
- cmt_prop.extend([cmt.make_element() for cmt in cmts])
482
- return cmt_prop
483
-
484
-
485
- def _validate_segment(segment: AudioSegmentResource | VideoSegmentResource) -> None:
486
- problems = []
487
- if not is_string_like(segment.res_id):
488
- problems.append(f"Field: Resource ID | Value: {segment.res_id}")
489
- if not is_string_like(segment.label):
490
- problems.append(f"Field: label | Value: {segment.label}")
491
- if not is_string_like(segment.segment_of):
492
- problems.append(f"Field: segment_of | Value: {segment.segment_of}")
493
- if segment.title and not is_string_like(segment.title):
494
- problems.append(f"Field: title | Value: {segment.title}")
495
- if fails := [x for x in segment.comments if not is_string_like(x)]:
496
- problems.extend([f"Field: comment | Value: {x}" for x in fails])
497
- if fails := [x for x in segment.descriptions if not is_string_like(x)]:
498
- problems.extend([f"Field: description | Value: {x}" for x in fails])
499
- if fails := [x for x in segment.keywords if not is_string_like(x)]:
500
- problems.extend([f"Field: keywords | Value: {x}" for x in fails])
501
- if fails := [x for x in segment.relates_to if not is_string_like(x)]:
502
- problems.extend([f"Field: relates_to | Value: {x}" for x in fails])
503
- if problems:
504
- msg = f"The resource with the ID '{segment.res_id}' has the following problem(s):{'\n- '.join(problems)}"
505
- warnings.warn(DspToolsUserWarning(msg))
506
-
507
-
508
- def _serialise_segment_children(segment: AudioSegmentResource | VideoSegmentResource) -> list[etree._Element]:
509
- segment_elements = []
510
- segment_of = etree.Element(f"{DASCH_SCHEMA}isSegmentOf", nsmap=XML_NAMESPACE_MAP)
511
- segment_of.text = segment.segment_of
512
- segment_elements.append(segment_of)
513
- segment_elements.append(
514
- etree.Element(
515
- f"{DASCH_SCHEMA}hasSegmentBounds",
516
- attrib={"start": str(segment.segment_bounds.segment_start), "end": str(segment.segment_bounds.segment_end)},
517
- nsmap=XML_NAMESPACE_MAP,
518
- )
519
- )
520
- if segment.title:
521
- segment_elements.append(_make_element_with_text("hasTitle", segment.title))
522
- segment_elements.extend([_make_element_with_text("hasComment", x) for x in segment.comments])
523
- segment_elements.extend([_make_element_with_text("hasDescription", x) for x in segment.descriptions])
524
- segment_elements.extend([_make_element_with_text("hasKeyword", x) for x in segment.keywords])
525
- segment_elements.extend([_make_element_with_text("relatesTo", x) for x in segment.relates_to])
526
- return segment_elements
527
-
528
-
529
- def _make_element_with_text(tag_name: str, text_content: str) -> etree._Element:
530
- ele = etree.Element(f"{DASCH_SCHEMA}{tag_name}", nsmap=XML_NAMESPACE_MAP)
531
- ele.text = text_content
532
- return ele
533
-
534
-
535
- def _transform_unexpected_input(value: Any, prop_name: str, res_id: str) -> list[str]:
536
- match value:
537
- case list():
538
- return value
539
- case set() | tuple():
540
- return list(value)
541
- case str():
542
- return [value]
543
- case _:
544
- _warn_unexpected_value(value, prop_name, res_id)
545
- return [str(value)]
546
-
547
-
548
- def _warn_unexpected_value(value: Any, prop_name: str, res_id: str | None) -> None:
549
- msg = (
550
- f"The resource: {res_id} should have a list of strings for the field '{prop_name}'. "
551
- f"Your input: '{value}' is of type {type(value)}."
552
- )
553
- warnings.warn(DspToolsUserWarning(msg))
1468
+ return self
1469
+
1470
+ def add_relates_to_multiple(
1471
+ self,
1472
+ relates_to: Collection[str],
1473
+ permissions: Permissions = Permissions.PROJECT_SPECIFIC_PERMISSIONS,
1474
+ comment: str | None = None,
1475
+ ) -> AudioSegmentResource:
1476
+ """
1477
+ Add several links to related resources
1478
+
1479
+ Args:
1480
+ relates_to: list of IDs of the related resources
1481
+ permissions: optional permissions of these values
1482
+ comment: optional comment
1483
+
1484
+ Returns:
1485
+ The original resource, with the added related resources
1486
+
1487
+ Examples:
1488
+ ```python
1489
+ audio_segment = audio_segment.add_relates_to_multiple(["target_resource_id_1", "target_resource_id_2"])
1490
+ ```
1491
+ """
1492
+ vals = check_and_fix_collection_input(relates_to, "relatesTo", self.res_id)
1493
+ for v in vals:
1494
+ self.add_relates_to(v, permissions, comment)
1495
+ return self
1496
+
1497
+ def add_relates_to_optional(
1498
+ self,
1499
+ relates_to: Any,
1500
+ permissions: Permissions = Permissions.PROJECT_SPECIFIC_PERMISSIONS,
1501
+ comment: str | None = None,
1502
+ ) -> AudioSegmentResource:
1503
+ """
1504
+ If the value is not empty, add it as related resource, otherwise return the resource unchanged.
1505
+
1506
+ Args:
1507
+ relates_to: ID of the related resource or empty value
1508
+ permissions: optional permissions of this value
1509
+ comment: optional comment
1510
+
1511
+ Returns:
1512
+ The original resource, with the added related resources
1513
+
1514
+ Examples:
1515
+ ```python
1516
+ audio_segment = audio_segment.add_relates_to_optional("target_resource_id")
1517
+ ```
1518
+
1519
+ ```python
1520
+ audio_segment = audio_segment.add_relates_to_optional(None)
1521
+ ```
1522
+ """
1523
+ if is_nonempty_value(relates_to):
1524
+ self.add_relates_to(relates_to, permissions, comment)
1525
+ return self
1526
+
1527
+
1528
+ def _check_strings(
1529
+ *, string_to_check: str, res_id: str, prop_name: str | None = None, field_name: str | None = None
1530
+ ) -> None:
1531
+ if not is_nonempty_value(string_to_check):
1532
+ msg_info = MessageInfo("The entered string is not valid.", res_id, prop_name, field_name)
1533
+ emit_xmllib_input_warning(msg_info)
554
1534
 
555
1535
 
556
- def _warn_value_exists(old_value: Any, new_value: Any, value_field: str, res_id: str | None) -> None:
557
- """Emits a warning if a values is not in the expected format."""
1536
+ def _warn_value_exists(*, old_value: Any, new_value: Any, res_id: str | None, value_field: str | None = None) -> None:
558
1537
  msg = (
559
- f"The resource with the ID '{res_id}' already has a value in the field '{value_field}'. "
1538
+ f"This resource already has a value in this location. "
560
1539
  f"The old value '{old_value}' is being replace with '{new_value}'."
561
1540
  )
562
- warnings.warn(DspToolsUserWarning(msg))
1541
+ msg_info = MessageInfo(msg, res_id, field=value_field)
1542
+ emit_xmllib_input_warning(msg_info)