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.
- dsp_tools/__init__.py +4 -0
- dsp_tools/cli/args.py +36 -0
- dsp_tools/cli/call_action.py +51 -231
- 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 +156 -58
- dsp_tools/cli/entry_point.py +56 -26
- 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/{utils → clients}/connection.py +2 -18
- 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 +57 -23
- dsp_tools/commands/excel2json/{new_lists → lists}/compliance_checks.py +26 -26
- dsp_tools/commands/excel2json/{new_lists/make_new_lists.py → lists/make_lists.py} +19 -18
- dsp_tools/commands/excel2json/{new_lists → lists}/models/input_error.py +1 -12
- dsp_tools/commands/excel2json/{new_lists → lists}/models/serialise.py +9 -5
- dsp_tools/commands/excel2json/{new_lists → lists}/utils.py +4 -4
- dsp_tools/commands/excel2json/models/input_error.py +31 -11
- dsp_tools/commands/excel2json/models/json_header.py +53 -15
- dsp_tools/commands/excel2json/models/ontology.py +4 -3
- dsp_tools/commands/excel2json/{lists.py → old_lists.py} +26 -112
- dsp_tools/commands/excel2json/project.py +78 -34
- dsp_tools/commands/excel2json/properties.py +57 -36
- dsp_tools/commands/excel2json/resources.py +32 -12
- dsp_tools/commands/excel2json/utils.py +20 -1
- dsp_tools/commands/excel2xml/__init__.py +2 -2
- dsp_tools/commands/excel2xml/excel2xml_cli.py +7 -15
- dsp_tools/commands/excel2xml/excel2xml_lib.py +138 -493
- dsp_tools/commands/excel2xml/propertyelement.py +5 -5
- dsp_tools/commands/{project → get}/get.py +29 -13
- dsp_tools/commands/get/get_permissions.py +257 -0
- dsp_tools/commands/get/get_permissions_legacy.py +89 -0
- dsp_tools/commands/{project/models → get/legacy_models}/context.py +6 -6
- dsp_tools/commands/{project/models → get/legacy_models}/group.py +5 -10
- dsp_tools/commands/{project/models → get/legacy_models}/listnode.py +5 -35
- dsp_tools/commands/{project/models → get/legacy_models}/model.py +1 -1
- dsp_tools/commands/{project/models → get/legacy_models}/ontology.py +9 -14
- dsp_tools/commands/{project/models → get/legacy_models}/project.py +13 -6
- dsp_tools/commands/{project/models → get/legacy_models}/propertyclass.py +9 -16
- dsp_tools/commands/{project/models → get/legacy_models}/resourceclass.py +8 -46
- dsp_tools/commands/{project/models → get/legacy_models}/user.py +19 -60
- dsp_tools/commands/get/models/permissions_models.py +10 -0
- dsp_tools/commands/id2iri.py +20 -10
- dsp_tools/commands/ingest_xmlupload/bulk_ingest_client.py +81 -56
- dsp_tools/commands/ingest_xmlupload/create_resources/apply_ingest_id.py +4 -10
- dsp_tools/commands/ingest_xmlupload/create_resources/upload_xml.py +97 -37
- dsp_tools/commands/ingest_xmlupload/create_resources/user_information.py +2 -2
- dsp_tools/commands/ingest_xmlupload/ingest_files/ingest_files.py +9 -10
- dsp_tools/commands/ingest_xmlupload/upload_files/filechecker.py +3 -3
- dsp_tools/commands/ingest_xmlupload/upload_files/input_error.py +2 -10
- dsp_tools/commands/ingest_xmlupload/upload_files/upload_failures.py +12 -2
- dsp_tools/commands/ingest_xmlupload/upload_files/upload_files.py +8 -9
- dsp_tools/commands/resume_xmlupload/resume_xmlupload.py +18 -18
- dsp_tools/commands/start_stack.py +126 -77
- 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/{xml_validate/sparql/resource_shacl.py → validate_data/sparql/cardinality_shacl.py} +45 -47
- 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/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/bitstream_info.py +18 -0
- dsp_tools/commands/xmlupload/models/formatted_text_value.py +0 -25
- dsp_tools/commands/xmlupload/models/ingest.py +56 -70
- dsp_tools/commands/xmlupload/models/input_problems.py +6 -14
- dsp_tools/commands/xmlupload/models/lookup_models.py +21 -0
- dsp_tools/commands/xmlupload/models/permission.py +0 -39
- dsp_tools/commands/xmlupload/models/{deserialise/xmlpermission.py → permissions_parsed.py} +2 -2
- 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 +3 -3
- dsp_tools/commands/xmlupload/models/upload_state.py +2 -4
- dsp_tools/commands/xmlupload/prepare_xml_input/__init__.py +0 -0
- dsp_tools/commands/xmlupload/{ark2iri.py → prepare_xml_input/ark2iri.py} +1 -1
- dsp_tools/commands/xmlupload/prepare_xml_input/get_processed_resources.py +252 -0
- dsp_tools/commands/xmlupload/{iiif_uri_validator.py → prepare_xml_input/iiif_uri_validator.py} +2 -14
- dsp_tools/commands/xmlupload/{list_client.py → prepare_xml_input/list_client.py} +15 -10
- 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 +7 -468
- dsp_tools/commands/xmlupload/richtext_id2iri.py +37 -0
- dsp_tools/commands/xmlupload/stash/{construct_and_analyze_graph.py → analyse_circular_reference_graph.py} +64 -157
- dsp_tools/commands/xmlupload/stash/create_info_for_graph.py +53 -0
- dsp_tools/commands/xmlupload/stash/graph_models.py +13 -8
- dsp_tools/commands/xmlupload/stash/stash_circular_references.py +48 -115
- dsp_tools/commands/xmlupload/stash/stash_models.py +4 -9
- dsp_tools/commands/xmlupload/stash/upload_stashed_resptr_props.py +34 -40
- dsp_tools/commands/xmlupload/stash/upload_stashed_xml_texts.py +98 -108
- dsp_tools/commands/xmlupload/upload_config.py +8 -0
- dsp_tools/commands/xmlupload/write_diagnostic_info.py +14 -9
- dsp_tools/commands/xmlupload/xmlupload.py +214 -192
- dsp_tools/config/__init__.py +0 -0
- dsp_tools/config/logger_config.py +69 -0
- dsp_tools/{utils → config}/warnings_config.py +4 -1
- 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/{models → legacy_models}/datetimestamp.py +7 -7
- dsp_tools/{models → legacy_models}/langstring.py +1 -1
- dsp_tools/{models → legacy_models}/projectContext.py +4 -4
- dsp_tools/resources/schema/data.xsd +108 -83
- dsp_tools/resources/schema/lists-only.json +4 -23
- dsp_tools/resources/schema/project.json +80 -35
- dsp_tools/resources/schema/properties-only.json +1 -4
- dsp_tools/resources/start-stack/docker-compose.override-host.j2 +11 -0
- dsp_tools/resources/start-stack/docker-compose.yml +34 -30
- 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/ansi_colors.py +32 -0
- dsp_tools/utils/data_formats/__init__.py +0 -0
- dsp_tools/utils/{date_util.py → data_formats/date_util.py} +13 -1
- dsp_tools/utils/data_formats/iri_util.py +30 -0
- dsp_tools/utils/{shared.py → data_formats/shared.py} +1 -35
- dsp_tools/utils/{uri_util.py → data_formats/uri_util.py} +12 -2
- 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/config_options.py +28 -0
- dsp_tools/xmllib/models/date_formats.py +48 -0
- dsp_tools/xmllib/models/dsp_base_resources.py +1380 -400
- 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/{migration_metadata.py → internal/migration_metadata.py} +14 -10
- 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 +313 -26
- dsp_tools/xmllib/value_checkers.py +310 -47
- dsp_tools/xmllib/value_converters.py +765 -8
- 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-9.1.0.post11.dist-info → dsp_tools-18.3.0.post13.dist-info}/entry_points.txt +1 -0
- dsp_tools/commands/project/create/project_create.py +0 -1107
- dsp_tools/commands/project/create/project_create_lists.py +0 -204
- dsp_tools/commands/project/create/project_validate.py +0 -453
- dsp_tools/commands/project/models/project_definition.py +0 -12
- dsp_tools/commands/rosetta.py +0 -124
- dsp_tools/commands/template.py +0 -30
- dsp_tools/commands/xml_validate/api_connection.py +0 -122
- dsp_tools/commands/xml_validate/deserialise_input.py +0 -135
- dsp_tools/commands/xml_validate/make_data_rdf.py +0 -193
- dsp_tools/commands/xml_validate/models/data_deserialised.py +0 -108
- dsp_tools/commands/xml_validate/models/data_rdf.py +0 -214
- dsp_tools/commands/xml_validate/models/input_problems.py +0 -191
- dsp_tools/commands/xml_validate/models/validation.py +0 -29
- dsp_tools/commands/xml_validate/reformat_validaton_result.py +0 -89
- dsp_tools/commands/xml_validate/sparql/construct_shapes.py +0 -16
- dsp_tools/commands/xml_validate/xml_validate.py +0 -151
- dsp_tools/commands/xmlupload/check_consistency_with_ontology.py +0 -253
- dsp_tools/commands/xmlupload/models/deserialise/deserialise_value.py +0 -236
- dsp_tools/commands/xmlupload/models/deserialise/xmlresource.py +0 -171
- dsp_tools/commands/xmlupload/models/namespace_context.py +0 -39
- dsp_tools/commands/xmlupload/models/ontology_lookup_models.py +0 -161
- dsp_tools/commands/xmlupload/models/ontology_problem_models.py +0 -178
- dsp_tools/commands/xmlupload/models/serialise/jsonld_serialiser.py +0 -40
- dsp_tools/commands/xmlupload/models/serialise/serialise_value.py +0 -51
- dsp_tools/commands/xmlupload/ontology_client.py +0 -92
- dsp_tools/commands/xmlupload/project_client.py +0 -91
- dsp_tools/commands/xmlupload/read_validate_xml_file.py +0 -99
- dsp_tools/models/custom_warnings.py +0 -31
- dsp_tools/models/exceptions.py +0 -90
- dsp_tools/resources/0100-template-repo/template.json +0 -45
- dsp_tools/resources/0100-template-repo/template.xml +0 -27
- dsp_tools/resources/start-stack/docker-compose-validation.yml +0 -5
- dsp_tools/resources/start-stack/start-stack-config.yml +0 -4
- dsp_tools/resources/xml_validate/api-shapes.ttl +0 -411
- dsp_tools/resources/xml_validate/replace_namespace.xslt +0 -61
- dsp_tools/utils/connection_live.py +0 -383
- dsp_tools/utils/iri_util.py +0 -14
- dsp_tools/utils/logger_config.py +0 -41
- dsp_tools/utils/set_encoder.py +0 -20
- dsp_tools/utils/xml_utils.py +0 -145
- dsp_tools/utils/xml_validation.py +0 -197
- dsp_tools/utils/xml_validation_models.py +0 -68
- dsp_tools/xmllib/models/file_values.py +0 -78
- dsp_tools/xmllib/models/resource.py +0 -415
- dsp_tools/xmllib/models/values.py +0 -428
- dsp_tools-9.1.0.post11.dist-info/METADATA +0 -130
- dsp_tools-9.1.0.post11.dist-info/RECORD +0 -167
- dsp_tools-9.1.0.post11.dist-info/WHEEL +0 -4
- dsp_tools-9.1.0.post11.dist-info/licenses/LICENSE +0 -674
- /dsp_tools/{commands/excel2json/new_lists → clients}/__init__.py +0 -0
- /dsp_tools/commands/{excel2json/new_lists/models → create}/__init__.py +0 -0
- /dsp_tools/commands/{project → create/create_on_server}/__init__.py +0 -0
- /dsp_tools/commands/{project/create → create/models}/__init__.py +0 -0
- /dsp_tools/commands/{project/models → create/parsing}/__init__.py +0 -0
- /dsp_tools/commands/{xml_validate → create/serialisation}/__init__.py +0 -0
- /dsp_tools/commands/{xml_validate/models → excel2json/lists}/__init__.py +0 -0
- /dsp_tools/commands/{xml_validate/sparql → excel2json/lists/models}/__init__.py +0 -0
- /dsp_tools/commands/excel2json/{new_lists → lists}/models/deserialise.py +0 -0
- /dsp_tools/commands/{xmlupload/models/deserialise → get}/__init__.py +0 -0
- /dsp_tools/commands/{xmlupload/models/serialise → get/legacy_models}/__init__.py +0 -0
- /dsp_tools/commands/{project/models → get/legacy_models}/helpers.py +0 -0
- /dsp_tools/{models → commands/get/models}/__init__.py +0 -0
|
@@ -1,204 +0,0 @@
|
|
|
1
|
-
from typing import Any
|
|
2
|
-
from typing import Optional
|
|
3
|
-
from typing import Union
|
|
4
|
-
|
|
5
|
-
from loguru import logger
|
|
6
|
-
|
|
7
|
-
from dsp_tools.cli.args import ServerCredentials
|
|
8
|
-
from dsp_tools.commands.excel2json.lists import expand_lists_from_excel
|
|
9
|
-
from dsp_tools.commands.project.create.project_validate import validate_project
|
|
10
|
-
from dsp_tools.commands.project.models.listnode import ListNode
|
|
11
|
-
from dsp_tools.commands.project.models.project import Project
|
|
12
|
-
from dsp_tools.models.exceptions import BaseError
|
|
13
|
-
from dsp_tools.models.exceptions import UserError
|
|
14
|
-
from dsp_tools.utils.connection import Connection
|
|
15
|
-
from dsp_tools.utils.connection_live import ConnectionLive
|
|
16
|
-
from dsp_tools.utils.shared import parse_json_input
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
def _create_list_node(
|
|
20
|
-
con: Connection,
|
|
21
|
-
project: Project,
|
|
22
|
-
node: dict[str, Any],
|
|
23
|
-
parent_node: Optional[ListNode] = None,
|
|
24
|
-
) -> tuple[dict[str, Any], bool]:
|
|
25
|
-
"""
|
|
26
|
-
Creates a list node on the DSP server, recursively scanning through all its subnodes, creating them as well.
|
|
27
|
-
If a node cannot be created, an error message is printed, but the process continues.
|
|
28
|
-
|
|
29
|
-
Args:
|
|
30
|
-
con: connection to the DSP server
|
|
31
|
-
project: project that holds the list where this node should be added to
|
|
32
|
-
node: the node to be created
|
|
33
|
-
parent_node: parent node of the node to be created (optional)
|
|
34
|
-
|
|
35
|
-
Returns:
|
|
36
|
-
Returns a tuple consisting of a dict and a bool.
|
|
37
|
-
The dict contains the IRIs of the created list nodes,
|
|
38
|
-
nested according to their hierarchy structure,
|
|
39
|
-
i.e. ``{nodename: {"id": IRI, "nodes": {...}}}``.
|
|
40
|
-
The bool is True if all nodes could be created,
|
|
41
|
-
False if any node could not be created.
|
|
42
|
-
|
|
43
|
-
Raises:
|
|
44
|
-
BaseError: if the created node has no name
|
|
45
|
-
"""
|
|
46
|
-
new_node: ListNode = ListNode(
|
|
47
|
-
con=con,
|
|
48
|
-
project=project,
|
|
49
|
-
label=node["labels"],
|
|
50
|
-
comments=node.get("comments"),
|
|
51
|
-
name=node["name"],
|
|
52
|
-
parent=parent_node,
|
|
53
|
-
)
|
|
54
|
-
try:
|
|
55
|
-
new_node = new_node.create()
|
|
56
|
-
except BaseError:
|
|
57
|
-
print(f"WARNING: Cannot create list node '{node['name']}'.")
|
|
58
|
-
logger.opt(exception=True).warning("Cannot create list node '{node['name']}'.")
|
|
59
|
-
return {}, False
|
|
60
|
-
|
|
61
|
-
# if node has child nodes, call the method recursively
|
|
62
|
-
if node.get("nodes"):
|
|
63
|
-
overall_success = True
|
|
64
|
-
subnode_list = []
|
|
65
|
-
for subnode in node["nodes"]:
|
|
66
|
-
created_subnode, success = _create_list_node(con=con, project=project, node=subnode, parent_node=new_node)
|
|
67
|
-
subnode_list.append(created_subnode)
|
|
68
|
-
if not success:
|
|
69
|
-
overall_success = False
|
|
70
|
-
if not new_node.name:
|
|
71
|
-
raise BaseError(f"Node {new_node} has no name.")
|
|
72
|
-
return {new_node.name: {"id": new_node.iri, "nodes": subnode_list}}, overall_success
|
|
73
|
-
else:
|
|
74
|
-
if not new_node.name:
|
|
75
|
-
raise BaseError(f"Node {new_node} has no name.")
|
|
76
|
-
return {new_node.name: {"id": new_node.iri}}, True
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
def create_lists_on_server(
|
|
80
|
-
lists_to_create: list[dict[str, Any]],
|
|
81
|
-
con: Connection,
|
|
82
|
-
project_remote: Project,
|
|
83
|
-
) -> tuple[dict[str, Any], bool]:
|
|
84
|
-
"""
|
|
85
|
-
Creates the "lists" section of a JSON project definition on a DSP server.
|
|
86
|
-
If a list with the same name is already existing in this project on the DSP server, this list is skipped.
|
|
87
|
-
If a node or an entire list cannot be created, an error message is printed, but the process continues.
|
|
88
|
-
|
|
89
|
-
Args:
|
|
90
|
-
lists_to_create: "lists" section of a JSON project definition
|
|
91
|
-
con: connection to the DSP server
|
|
92
|
-
project_remote: representation of the project on the DSP server
|
|
93
|
-
|
|
94
|
-
Raises:
|
|
95
|
-
BaseError: if one of the lists to be created already exists on the DSP server, but it has no name
|
|
96
|
-
|
|
97
|
-
Returns:
|
|
98
|
-
tuple consisting of the IRIs of the list nodes and the success status (True if everything went well)
|
|
99
|
-
"""
|
|
100
|
-
|
|
101
|
-
overall_success = True
|
|
102
|
-
|
|
103
|
-
# retrieve existing lists
|
|
104
|
-
try:
|
|
105
|
-
existing_lists = ListNode.getAllLists(con=con, project_iri=project_remote.iri)
|
|
106
|
-
except BaseError:
|
|
107
|
-
err_msg = "Unable to retrieve existing lists on DSP server. Cannot check if your lists are already existing."
|
|
108
|
-
print(f"WARNING: {err_msg}")
|
|
109
|
-
logger.opt(exception=True).warning(err_msg)
|
|
110
|
-
existing_lists = []
|
|
111
|
-
overall_success = False
|
|
112
|
-
|
|
113
|
-
current_project_lists: dict[str, Any] = {}
|
|
114
|
-
for new_lst in lists_to_create:
|
|
115
|
-
if existing_lst := [x for x in existing_lists if x.project == project_remote.iri and x.name == new_lst["name"]]:
|
|
116
|
-
existing_list_name = existing_lst[0].name
|
|
117
|
-
if not existing_list_name:
|
|
118
|
-
raise BaseError(f"Node {existing_lst[0]} has no name.")
|
|
119
|
-
current_project_lists[existing_list_name] = {
|
|
120
|
-
"id": existing_lst[0].iri,
|
|
121
|
-
"nodes": new_lst["nodes"],
|
|
122
|
-
}
|
|
123
|
-
print(f" WARNING: List '{new_lst['name']}' already exists on the DSP server. Skipping...")
|
|
124
|
-
overall_success = False
|
|
125
|
-
continue
|
|
126
|
-
|
|
127
|
-
created_list, success = _create_list_node(con=con, project=project_remote, node=new_lst)
|
|
128
|
-
current_project_lists.update(created_list)
|
|
129
|
-
if not success:
|
|
130
|
-
overall_success = False
|
|
131
|
-
print(f" Created list '{new_lst['name']}'.")
|
|
132
|
-
|
|
133
|
-
return current_project_lists, overall_success
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
def create_lists(
|
|
137
|
-
project_file_as_path_or_parsed: Union[str, dict[str, Any]],
|
|
138
|
-
creds: ServerCredentials,
|
|
139
|
-
) -> tuple[dict[str, Any], bool]:
|
|
140
|
-
"""
|
|
141
|
-
This method accepts a JSON project definition,
|
|
142
|
-
expands the Excel sheets referenced in its "lists" section,
|
|
143
|
-
connects to a DSP server,
|
|
144
|
-
and uploads the "lists" section to the server.
|
|
145
|
-
|
|
146
|
-
The project must already exist on the DSP server.
|
|
147
|
-
If a list with the same name is already existing in this project on the DSP server, this list is skipped.
|
|
148
|
-
|
|
149
|
-
Args:
|
|
150
|
-
project_file_as_path_or_parsed: path to the JSON project definition, or parsed JSON object
|
|
151
|
-
creds: credentials to connect to the DSP server
|
|
152
|
-
|
|
153
|
-
Raises:
|
|
154
|
-
UserError:
|
|
155
|
-
- if the project cannot be read from the server
|
|
156
|
-
- if the connection to the DSP server cannot be established
|
|
157
|
-
|
|
158
|
-
BaseError:
|
|
159
|
-
- if the input is invalid
|
|
160
|
-
- if a problem occurred while trying to expand the Excel files
|
|
161
|
-
- if the JSON file is invalid according to the schema
|
|
162
|
-
|
|
163
|
-
Returns:
|
|
164
|
-
Returns a tuple consisting of a dict and a bool.
|
|
165
|
-
The dict contains the IRIs of the created list nodes,
|
|
166
|
-
nested according to their hierarchy structure,
|
|
167
|
-
i.e. ``{nodename: {"id": IRI, "nodes": {...}}}``.
|
|
168
|
-
If there are no lists in the project definition,
|
|
169
|
-
an empty dictionary is returned.
|
|
170
|
-
The bool indicates if everything went smoothly during the process.
|
|
171
|
-
If a warning or error occurred (e.g. one of the lists already exists,
|
|
172
|
-
or one of the nodes could not be created),
|
|
173
|
-
it is False.
|
|
174
|
-
"""
|
|
175
|
-
project_definition = parse_json_input(project_file_as_path_or_parsed=project_file_as_path_or_parsed)
|
|
176
|
-
if not project_definition.get("project", {}).get("lists"):
|
|
177
|
-
return {}, True
|
|
178
|
-
lists_to_create = expand_lists_from_excel(project_definition["project"]["lists"])
|
|
179
|
-
project_definition["project"]["lists"] = lists_to_create
|
|
180
|
-
validate_project(project_definition, expand_lists=False)
|
|
181
|
-
print("JSON project file is syntactically correct and passed validation.")
|
|
182
|
-
|
|
183
|
-
# connect to the DSP server
|
|
184
|
-
con = ConnectionLive(creds.server)
|
|
185
|
-
con.login(creds.user, creds.password)
|
|
186
|
-
|
|
187
|
-
# retrieve the project
|
|
188
|
-
shortcode = project_definition["project"]["shortcode"]
|
|
189
|
-
project_local = Project(con=con, shortcode=shortcode)
|
|
190
|
-
try:
|
|
191
|
-
project_remote = project_local.read()
|
|
192
|
-
except BaseError:
|
|
193
|
-
err_msg = f"Unable to create the lists: The project {shortcode} cannot be found on the DSP server."
|
|
194
|
-
logger.opt(exception=True).error(err_msg)
|
|
195
|
-
raise UserError(err_msg) from None
|
|
196
|
-
|
|
197
|
-
# create new lists
|
|
198
|
-
current_project_lists, success = create_lists_on_server(
|
|
199
|
-
lists_to_create=lists_to_create,
|
|
200
|
-
con=con,
|
|
201
|
-
project_remote=project_remote,
|
|
202
|
-
)
|
|
203
|
-
|
|
204
|
-
return current_project_lists, success
|
|
@@ -1,453 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import importlib.resources
|
|
4
|
-
import json
|
|
5
|
-
from pathlib import Path
|
|
6
|
-
from typing import Any
|
|
7
|
-
from typing import Union
|
|
8
|
-
|
|
9
|
-
import jsonpath_ng
|
|
10
|
-
import jsonpath_ng.ext
|
|
11
|
-
import jsonschema
|
|
12
|
-
import networkx as nx
|
|
13
|
-
import regex
|
|
14
|
-
|
|
15
|
-
from dsp_tools.commands.excel2json.lists import expand_lists_from_excel
|
|
16
|
-
from dsp_tools.models.exceptions import BaseError
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
def _check_for_duplicate_names(project_definition: dict[str, Any]) -> bool:
|
|
20
|
-
"""
|
|
21
|
-
Check that the resource names and property names are unique.
|
|
22
|
-
|
|
23
|
-
Args:
|
|
24
|
-
project_definition: parsed JSON project definition
|
|
25
|
-
|
|
26
|
-
Raises:
|
|
27
|
-
BaseError: detailed error message if there is a duplicate resource name / property name
|
|
28
|
-
|
|
29
|
-
Returns:
|
|
30
|
-
True if the resource/property names are unique
|
|
31
|
-
"""
|
|
32
|
-
propnames_duplicates, resnames_duplicates = _find_duplicates(project_definition)
|
|
33
|
-
|
|
34
|
-
if not resnames_duplicates and not propnames_duplicates:
|
|
35
|
-
return True
|
|
36
|
-
|
|
37
|
-
err_msg = "Resource names and property names must be unique inside every ontology.\n"
|
|
38
|
-
for ontoname, res_duplicates in resnames_duplicates.items():
|
|
39
|
-
for res_duplicate in sorted(res_duplicates):
|
|
40
|
-
err_msg += f"Resource '{res_duplicate}' appears multiple times in the ontology '{ontoname}'.\n"
|
|
41
|
-
for ontoname, prop_duplicates in propnames_duplicates.items():
|
|
42
|
-
for prop_duplicate in sorted(prop_duplicates):
|
|
43
|
-
err_msg += f"Property '{prop_duplicate}' appears multiple times in the ontology '{ontoname}'.\n"
|
|
44
|
-
|
|
45
|
-
raise BaseError(err_msg)
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
def _find_duplicates(project_definition: dict[str, Any]) -> tuple[dict[str, set[str]], dict[str, set[str]]]:
|
|
49
|
-
resnames_duplicates: dict[str, set[str]] = {}
|
|
50
|
-
propnames_duplicates: dict[str, set[str]] = {}
|
|
51
|
-
for onto in project_definition["project"]["ontologies"]:
|
|
52
|
-
resnames = [r["name"] for r in onto["resources"]]
|
|
53
|
-
if len(set(resnames)) != len(resnames):
|
|
54
|
-
for elem in resnames:
|
|
55
|
-
if resnames.count(elem) > 1:
|
|
56
|
-
if resnames_duplicates.get(onto["name"]):
|
|
57
|
-
resnames_duplicates[onto["name"]].add(elem)
|
|
58
|
-
else:
|
|
59
|
-
resnames_duplicates[onto["name"]] = {elem}
|
|
60
|
-
|
|
61
|
-
propnames = [p["name"] for p in onto["properties"]]
|
|
62
|
-
if len(set(propnames)) != len(propnames):
|
|
63
|
-
for elem in propnames:
|
|
64
|
-
if propnames.count(elem) > 1:
|
|
65
|
-
if propnames_duplicates.get(onto["name"]):
|
|
66
|
-
propnames_duplicates[onto["name"]].add(elem)
|
|
67
|
-
else:
|
|
68
|
-
propnames_duplicates[onto["name"]] = {elem}
|
|
69
|
-
return propnames_duplicates, resnames_duplicates
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
def _check_for_undefined_super_resource(project_definition: dict[str, Any]) -> bool:
|
|
73
|
-
"""
|
|
74
|
-
Check the superresources that claim to point to a resource defined in the same JSON project.
|
|
75
|
-
Check if the resource they point to actually exists.
|
|
76
|
-
(DSP base resources and resources from other ontologies are not considered.)
|
|
77
|
-
|
|
78
|
-
Args:
|
|
79
|
-
project_definition: parsed JSON project definition
|
|
80
|
-
|
|
81
|
-
Raises:
|
|
82
|
-
BaseError: detailed error message if a superresource is not existent
|
|
83
|
-
|
|
84
|
-
Returns:
|
|
85
|
-
True if the superresource are valid
|
|
86
|
-
"""
|
|
87
|
-
errors: dict[str, list[str]] = {}
|
|
88
|
-
for onto in project_definition["project"]["ontologies"]:
|
|
89
|
-
ontoname = onto["name"]
|
|
90
|
-
resnames = [r["name"] for r in onto["resources"]]
|
|
91
|
-
for res in onto["resources"]:
|
|
92
|
-
supers = res["super"] if isinstance(res["super"], list) else [res["super"]]
|
|
93
|
-
# form of supers:
|
|
94
|
-
# - Resource # DSP base resource
|
|
95
|
-
# - other:res # other onto
|
|
96
|
-
# - same:res # same onto
|
|
97
|
-
# - :res # same onto (short form)
|
|
98
|
-
|
|
99
|
-
# filter out DSP base resources
|
|
100
|
-
supers = [s for s in supers if ":" in s]
|
|
101
|
-
# extend short form
|
|
102
|
-
supers = [regex.sub(r"^:", f"{ontoname}:", s) for s in supers]
|
|
103
|
-
# filter out other ontos
|
|
104
|
-
supers = [s for s in supers if regex.search(f"^{ontoname}:", s)]
|
|
105
|
-
# convert to short form
|
|
106
|
-
supers = [regex.sub(f"^{ontoname}", "", s) for s in supers]
|
|
107
|
-
|
|
108
|
-
if invalid_references := [s for s in supers if regex.sub(":", "", s) not in resnames]:
|
|
109
|
-
errors[f"Ontology '{ontoname}', resource '{res['name']}'"] = invalid_references
|
|
110
|
-
|
|
111
|
-
if errors:
|
|
112
|
-
err_msg = "Your data model contains resources that are derived from an invalid super-resource:\n" + "\n".join(
|
|
113
|
-
f" - {loc}: {invalids}" for loc, invalids in errors.items()
|
|
114
|
-
)
|
|
115
|
-
raise BaseError(err_msg)
|
|
116
|
-
return True
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
def _check_for_undefined_super_property(project_definition: dict[str, Any]) -> bool:
|
|
120
|
-
"""
|
|
121
|
-
Check the superproperties that claim to point to a property defined in the same JSON project.
|
|
122
|
-
Check if the property they point to actually exists.
|
|
123
|
-
(DSP base properties and properties from other ontologies are not considered.)
|
|
124
|
-
|
|
125
|
-
Args:
|
|
126
|
-
project_definition: parsed JSON project definition
|
|
127
|
-
|
|
128
|
-
Raises:
|
|
129
|
-
BaseError: detailed error message if a superproperty is not existent
|
|
130
|
-
|
|
131
|
-
Returns:
|
|
132
|
-
True if the superproperties are valid
|
|
133
|
-
"""
|
|
134
|
-
errors: dict[str, list[str]] = {}
|
|
135
|
-
for onto in project_definition["project"]["ontologies"]:
|
|
136
|
-
ontoname = onto["name"]
|
|
137
|
-
propnames = [p["name"] for p in onto["properties"]]
|
|
138
|
-
for prop in onto["properties"]:
|
|
139
|
-
supers = prop["super"]
|
|
140
|
-
# form of supers:
|
|
141
|
-
# - isSegmentOf # DSP base property
|
|
142
|
-
# - other:prop # other onto
|
|
143
|
-
# - same:prop # same onto
|
|
144
|
-
# - :prop # same onto (short form)
|
|
145
|
-
|
|
146
|
-
# filter out DSP base properties
|
|
147
|
-
supers = [s for s in supers if ":" in s]
|
|
148
|
-
# extend short form
|
|
149
|
-
supers = [regex.sub(r"^:", f"{ontoname}:", s) for s in supers]
|
|
150
|
-
# filter out other ontos
|
|
151
|
-
supers = [s for s in supers if regex.search(f"^{ontoname}:", s)]
|
|
152
|
-
# convert to short form
|
|
153
|
-
supers = [regex.sub(f"^{ontoname}", "", s) for s in supers]
|
|
154
|
-
|
|
155
|
-
if invalid_references := [s for s in supers if regex.sub(":", "", s) not in propnames]:
|
|
156
|
-
errors[f"Ontology '{ontoname}', property '{prop['name']}'"] = invalid_references
|
|
157
|
-
|
|
158
|
-
if errors:
|
|
159
|
-
err_msg = "Your data model contains properties that are derived from an invalid super-property:\n" + "\n".join(
|
|
160
|
-
f" - {loc}: {invalids}" for loc, invalids in errors.items()
|
|
161
|
-
)
|
|
162
|
-
raise BaseError(err_msg)
|
|
163
|
-
return True
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
def _check_for_undefined_cardinalities(project_definition: dict[str, Any]) -> bool:
|
|
167
|
-
"""
|
|
168
|
-
Check if the propnames that are used in the cardinalities of each resource are defined in the "properties"
|
|
169
|
-
section. (DSP base properties and properties from other ontologies are not considered.)
|
|
170
|
-
|
|
171
|
-
Args:
|
|
172
|
-
project_definition: parsed JSON project definition
|
|
173
|
-
|
|
174
|
-
Raises:
|
|
175
|
-
BaseError: detailed error message if a cardinality is used that is not defined
|
|
176
|
-
|
|
177
|
-
Returns:
|
|
178
|
-
True if all cardinalities are defined in the "properties" section
|
|
179
|
-
"""
|
|
180
|
-
errors: dict[str, list[str]] = {}
|
|
181
|
-
for onto in project_definition["project"]["ontologies"]:
|
|
182
|
-
ontoname = onto["name"]
|
|
183
|
-
propnames = [prop["name"] for prop in onto["properties"]]
|
|
184
|
-
for res in onto["resources"]:
|
|
185
|
-
cardnames = [card["propname"] for card in res.get("cardinalities", [])]
|
|
186
|
-
# form of the cardnames:
|
|
187
|
-
# - isSegmentOf # DSP base property
|
|
188
|
-
# - other:prop # other onto
|
|
189
|
-
# - same:prop # same onto
|
|
190
|
-
# - :prop # same onto (short form)
|
|
191
|
-
|
|
192
|
-
# filter out DSP base properties
|
|
193
|
-
cardnames = [card for card in cardnames if ":" in card]
|
|
194
|
-
# extend short form
|
|
195
|
-
cardnames = [regex.sub(r"^:", f"{ontoname}:", card) for card in cardnames]
|
|
196
|
-
# filter out other ontos
|
|
197
|
-
cardnames = [card for card in cardnames if regex.search(f"^{ontoname}:", card)]
|
|
198
|
-
# convert to short form
|
|
199
|
-
cardnames = [regex.sub(f"^{ontoname}:", ":", card) for card in cardnames]
|
|
200
|
-
|
|
201
|
-
if invalid_cardnames := [card for card in cardnames if regex.sub(":", "", card) not in propnames]:
|
|
202
|
-
errors[f"Ontology '{ontoname}', resource '{res['name']}'"] = invalid_cardnames
|
|
203
|
-
|
|
204
|
-
if errors:
|
|
205
|
-
err_msg = "Your data model contains cardinalities with invalid propnames:\n" + "\n".join(
|
|
206
|
-
f" - {loc}: {invalids}" for loc, invalids in errors.items()
|
|
207
|
-
)
|
|
208
|
-
raise BaseError(err_msg)
|
|
209
|
-
return True
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
def _check_for_deprecated_syntax(project_definition: dict[str, Any]) -> bool: # noqa: ARG001 (unused argument)
|
|
213
|
-
return True
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
def validate_project(
|
|
217
|
-
input_file_or_json: Union[dict[str, Any], str],
|
|
218
|
-
expand_lists: bool = True,
|
|
219
|
-
) -> bool:
|
|
220
|
-
"""
|
|
221
|
-
Validates a JSON project definition file.
|
|
222
|
-
|
|
223
|
-
First, the Excel file references in the "lists" section are expanded
|
|
224
|
-
(unless this behaviour is disabled).
|
|
225
|
-
|
|
226
|
-
Then, the project is validated against the JSON schema.
|
|
227
|
-
|
|
228
|
-
Next, some checks are performed that are too complex for JSON schema.
|
|
229
|
-
|
|
230
|
-
At last, a check is performed
|
|
231
|
-
if this project's ontologies contain properties derived from hasLinkTo
|
|
232
|
-
that form a circular reference.
|
|
233
|
-
If so, these properties must have the cardinality 0-1 or 0-n,
|
|
234
|
-
because during the xmlupload process,
|
|
235
|
-
these values are temporarily removed.
|
|
236
|
-
|
|
237
|
-
Args:
|
|
238
|
-
input_file_or_json: the project to be validated, can either be a file path or a parsed JSON file
|
|
239
|
-
expand_lists: if True, the Excel file references in the "lists" section will be expanded
|
|
240
|
-
|
|
241
|
-
Raises:
|
|
242
|
-
BaseError: detailed error report if the validation doesn't pass
|
|
243
|
-
|
|
244
|
-
Returns:
|
|
245
|
-
True if the project passed validation.
|
|
246
|
-
"""
|
|
247
|
-
|
|
248
|
-
# parse input
|
|
249
|
-
if isinstance(input_file_or_json, dict) and "project" in input_file_or_json:
|
|
250
|
-
project_definition = input_file_or_json
|
|
251
|
-
elif (
|
|
252
|
-
isinstance(input_file_or_json, str)
|
|
253
|
-
and Path(input_file_or_json).is_file()
|
|
254
|
-
and regex.search(r"\.json$", input_file_or_json)
|
|
255
|
-
):
|
|
256
|
-
with open(input_file_or_json, encoding="utf-8") as f:
|
|
257
|
-
project_definition = json.load(f)
|
|
258
|
-
else:
|
|
259
|
-
raise BaseError(f"Input '{input_file_or_json}' is neither a file path nor a JSON object.")
|
|
260
|
-
|
|
261
|
-
# expand all lists referenced in the "lists" section of the project definition,
|
|
262
|
-
# and add them to the project definition
|
|
263
|
-
if expand_lists:
|
|
264
|
-
if new_lists := expand_lists_from_excel(project_definition["project"].get("lists", [])):
|
|
265
|
-
project_definition["project"]["lists"] = new_lists
|
|
266
|
-
|
|
267
|
-
# validate the project definition against the schema
|
|
268
|
-
with (
|
|
269
|
-
importlib.resources.files("dsp_tools")
|
|
270
|
-
.joinpath("resources/schema/project.json")
|
|
271
|
-
.open(encoding="utf-8") as schema_file
|
|
272
|
-
):
|
|
273
|
-
project_schema = json.load(schema_file)
|
|
274
|
-
try:
|
|
275
|
-
jsonschema.validate(instance=project_definition, schema=project_schema)
|
|
276
|
-
except jsonschema.ValidationError as err:
|
|
277
|
-
raise BaseError(
|
|
278
|
-
f"The JSON project file cannot be created due to the following validation error: {err.message}.\n"
|
|
279
|
-
f"The error occurred at {err.json_path}:\n{err.instance}"
|
|
280
|
-
) from None
|
|
281
|
-
|
|
282
|
-
# make some checks that are too complex for JSON schema
|
|
283
|
-
_check_for_undefined_super_property(project_definition)
|
|
284
|
-
_check_for_undefined_super_resource(project_definition)
|
|
285
|
-
_check_for_undefined_cardinalities(project_definition)
|
|
286
|
-
_check_for_duplicate_names(project_definition)
|
|
287
|
-
_check_for_deprecated_syntax(project_definition)
|
|
288
|
-
|
|
289
|
-
# cardinalities check for circular references
|
|
290
|
-
return _check_cardinalities_of_circular_references(project_definition)
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
def _check_cardinalities_of_circular_references(project_definition: dict[Any, Any]) -> bool:
|
|
294
|
-
"""
|
|
295
|
-
Check a JSON project file if it contains properties derived from hasLinkTo that form a circular reference. If so,
|
|
296
|
-
these properties must have the cardinality 0-1 or 0-n, because during the xmlupload process, these values
|
|
297
|
-
are temporarily removed.
|
|
298
|
-
|
|
299
|
-
Args:
|
|
300
|
-
project_definition: dictionary with a DSP project (as defined in a JSON project file)
|
|
301
|
-
|
|
302
|
-
Raises:
|
|
303
|
-
BaseError: if there is a circle with at least one element that has a cardinality of "1" or "1-n"
|
|
304
|
-
|
|
305
|
-
Returns:
|
|
306
|
-
True if no circle was detected, or if all elements of all circles are of cardinality "0-1" or "0-n".
|
|
307
|
-
"""
|
|
308
|
-
|
|
309
|
-
link_properties = _collect_link_properties(project_definition)
|
|
310
|
-
errors = _identify_problematic_cardinalities(project_definition, link_properties)
|
|
311
|
-
|
|
312
|
-
if len(errors) == 0:
|
|
313
|
-
return True
|
|
314
|
-
|
|
315
|
-
error_message = (
|
|
316
|
-
"ERROR: Your ontology contains properties derived from 'hasLinkTo' that allow circular references "
|
|
317
|
-
"between resources. This is not a problem in itself, but if you try to upload data that actually "
|
|
318
|
-
"contains circular references, these 'hasLinkTo' properties will be temporarily removed from the "
|
|
319
|
-
"affected resources. Therefore, it is necessary that all involved 'hasLinkTo' properties have a "
|
|
320
|
-
"cardinality of 0-1 or 0-n. \n"
|
|
321
|
-
"Please make sure that the following properties have a cardinality of 0-1 or 0-n:"
|
|
322
|
-
)
|
|
323
|
-
for error in errors:
|
|
324
|
-
error_message = f"{error_message}\n - Resource {error[0]}, property {error[1]}"
|
|
325
|
-
raise BaseError(error_message)
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
def _collect_link_properties(project_definition: dict[Any, Any]) -> dict[str, list[str]]:
|
|
329
|
-
"""
|
|
330
|
-
Maps the properties derived from hasLinkTo to the resource classes they point to.
|
|
331
|
-
|
|
332
|
-
Args:
|
|
333
|
-
project_definition: parsed JSON file
|
|
334
|
-
|
|
335
|
-
Returns:
|
|
336
|
-
A (possibly empty) dictionary in the form {"rosetta:hasImage2D": ["rosetta:Image2D"], ...}
|
|
337
|
-
"""
|
|
338
|
-
ontos = project_definition["project"]["ontologies"]
|
|
339
|
-
hasLinkTo_props = {"hasLinkTo", "isPartOf", "isRegionOf", "isAnnotationOf"}
|
|
340
|
-
link_properties: dict[str, list[str]] = {}
|
|
341
|
-
for index, onto in enumerate(ontos):
|
|
342
|
-
hasLinkTo_matches = []
|
|
343
|
-
# look for child-properties down to 5 inheritance levels that are derived from hasLinkTo-properties
|
|
344
|
-
for _ in range(5):
|
|
345
|
-
for hasLinkTo_prop in hasLinkTo_props:
|
|
346
|
-
hasLinkTo_matches.extend(
|
|
347
|
-
jsonpath_ng.ext.parse(
|
|
348
|
-
f"$.project.ontologies[{index}].properties[?super[*] == {hasLinkTo_prop}]"
|
|
349
|
-
).find(project_definition)
|
|
350
|
-
)
|
|
351
|
-
# make the children from this iteration to the parents of the next iteration
|
|
352
|
-
hasLinkTo_props = {x.value["name"] for x in hasLinkTo_matches}
|
|
353
|
-
prop_obj_pair: dict[str, list[str]] = {}
|
|
354
|
-
for match in hasLinkTo_matches:
|
|
355
|
-
prop = onto["name"] + ":" + match.value["name"]
|
|
356
|
-
target = match.value["object"]
|
|
357
|
-
if target != "Resource":
|
|
358
|
-
# make the target a fully qualified name (with the ontology's name prefixed)
|
|
359
|
-
target = regex.sub(r"^:([^:]+)$", f"{onto['name']}:\\1", target)
|
|
360
|
-
prop_obj_pair[prop] = [target]
|
|
361
|
-
link_properties.update(prop_obj_pair)
|
|
362
|
-
|
|
363
|
-
# in case the object of a property is "Resource", the link can point to any resource class
|
|
364
|
-
all_res_names: list[str] = []
|
|
365
|
-
for onto in ontos:
|
|
366
|
-
matches = jsonpath_ng.ext.parse("$.resources[*].name").find(onto)
|
|
367
|
-
tmp = [f"{onto['name']}:{match.value}" for match in matches]
|
|
368
|
-
all_res_names.extend(tmp)
|
|
369
|
-
for prop, targ in link_properties.items():
|
|
370
|
-
if "Resource" in targ:
|
|
371
|
-
link_properties[prop] = all_res_names
|
|
372
|
-
|
|
373
|
-
return link_properties
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
def _identify_problematic_cardinalities(
|
|
377
|
-
project_definition: dict[Any, Any],
|
|
378
|
-
link_properties: dict[str, list[str]],
|
|
379
|
-
) -> list[tuple[str, str]]:
|
|
380
|
-
"""
|
|
381
|
-
Make an error list with all cardinalities that are part of a circle but have a cardinality of "1" or "1-n".
|
|
382
|
-
|
|
383
|
-
Args:
|
|
384
|
-
project_definition: parsed JSON file
|
|
385
|
-
link_properties: mapping of hasLinkTo-properties to classes they point to,
|
|
386
|
-
e.g. {"rosetta:hasImage2D": ["rosetta:Image2D"], ...}
|
|
387
|
-
|
|
388
|
-
Returns:
|
|
389
|
-
a (possibly empty) list of (resource, problematic_cardinality) tuples
|
|
390
|
-
"""
|
|
391
|
-
cardinalities, dependencies = _extract_cardinalities_from_project(project_definition, link_properties)
|
|
392
|
-
graph = _make_cardinality_dependency_graph(dependencies)
|
|
393
|
-
errors = _find_circles_with_min_one_cardinality(graph, cardinalities, dependencies)
|
|
394
|
-
return sorted(errors, key=lambda x: x[0])
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
def _extract_cardinalities_from_project(
|
|
398
|
-
project_definition: dict[Any, Any],
|
|
399
|
-
link_properties: dict[str, list[str]],
|
|
400
|
-
) -> tuple[dict[str, dict[str, str]], dict[str, dict[str, list[str]]]]:
|
|
401
|
-
# dependencies = {"rosetta:Text": {"rosetta:hasImage2D": ["rosetta:Image2D"], ...}}
|
|
402
|
-
dependencies: dict[str, dict[str, list[str]]] = {}
|
|
403
|
-
# cardinalities = {"rosetta:Text": {"rosetta:hasImage2D": "0-1", ...}}
|
|
404
|
-
cardinalities: dict[str, dict[str, str]] = {}
|
|
405
|
-
|
|
406
|
-
for onto in project_definition["project"]["ontologies"]:
|
|
407
|
-
for resource in onto["resources"]:
|
|
408
|
-
resname: str = onto["name"] + ":" + resource["name"]
|
|
409
|
-
for card in resource.get("cardinalities", []):
|
|
410
|
-
# make the cardinality a fully qualified name (with the ontology's name prefixed)
|
|
411
|
-
cardname = regex.sub(r"^(:?)([^:]+)$", f"{onto['name']}:\\2", card["propname"])
|
|
412
|
-
if cardname in link_properties:
|
|
413
|
-
# Look out: if `targets` is created with `targets = link_properties[cardname]`, the ex-
|
|
414
|
-
# pression `dependencies[resname][cardname] = targets` causes `dependencies[resname][cardname]`
|
|
415
|
-
# to point to `link_properties[cardname]`. Due to that, the expression
|
|
416
|
-
# `dependencies[resname][cardname].extend(targets)` will modify "link_properties"!
|
|
417
|
-
# For this reason, `targets` must be created with `targets = list(link_properties[cardname])`
|
|
418
|
-
targets = list(link_properties[cardname])
|
|
419
|
-
if resname not in dependencies:
|
|
420
|
-
dependencies[resname] = {cardname: targets}
|
|
421
|
-
cardinalities[resname] = {cardname: card["cardinality"]}
|
|
422
|
-
elif cardname not in dependencies[resname]:
|
|
423
|
-
dependencies[resname][cardname] = targets
|
|
424
|
-
cardinalities[resname][cardname] = card["cardinality"]
|
|
425
|
-
else:
|
|
426
|
-
dependencies[resname][cardname].extend(targets)
|
|
427
|
-
return cardinalities, dependencies
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
def _make_cardinality_dependency_graph(dependencies: dict[str, dict[str, list[str]]]) -> nx.MultiDiGraph[Any]:
|
|
431
|
-
graph: nx.MultiDiGraph[Any] = nx.MultiDiGraph()
|
|
432
|
-
for start, cards in dependencies.items():
|
|
433
|
-
for edge, targets in cards.items():
|
|
434
|
-
for target in targets:
|
|
435
|
-
graph.add_edge(start, target, edge)
|
|
436
|
-
return graph
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
def _find_circles_with_min_one_cardinality(
|
|
440
|
-
graph: nx.MultiDiGraph[Any], cardinalities: dict[str, dict[str, str]], dependencies: dict[str, dict[str, list[str]]]
|
|
441
|
-
) -> set[tuple[str, str]]:
|
|
442
|
-
errors: set[tuple[str, str]] = set()
|
|
443
|
-
circles = list(nx.algorithms.cycles.simple_cycles(graph))
|
|
444
|
-
for circle in circles:
|
|
445
|
-
for index, resource in enumerate(circle):
|
|
446
|
-
target = circle[(index + 1) % len(circle)]
|
|
447
|
-
prop = ""
|
|
448
|
-
for _property, targets in dependencies[resource].items():
|
|
449
|
-
if target in targets:
|
|
450
|
-
prop = _property
|
|
451
|
-
if cardinalities[resource].get(prop) not in ["0-1", "0-n"]:
|
|
452
|
-
errors.add((resource, prop))
|
|
453
|
-
return errors
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
from dataclasses import dataclass
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
@dataclass
|
|
5
|
-
class ProjectDefinition:
|
|
6
|
-
shortcode: str
|
|
7
|
-
shortname: str
|
|
8
|
-
longname: str
|
|
9
|
-
keywords: list[str] | None = None
|
|
10
|
-
descriptions: dict[str, str] | None = None
|
|
11
|
-
groups: list[dict[str, str]] | None = None
|
|
12
|
-
users: list[dict[str, str]] | None = None
|