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