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