dsp-tools 17.0.0.post29__py3-none-any.whl → 18.0.0.post3__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.
Potentially problematic release.
This version of dsp-tools might be problematic. Click here for more details.
- dsp_tools/cli/args.py +13 -0
- dsp_tools/cli/call_action.py +34 -330
- dsp_tools/cli/call_action_files_only.py +74 -0
- dsp_tools/cli/call_action_with_network.py +202 -0
- dsp_tools/cli/create_parsers.py +53 -14
- dsp_tools/cli/utils.py +87 -0
- dsp_tools/clients/list_client.py +49 -0
- dsp_tools/clients/list_client_live.py +166 -0
- dsp_tools/clients/{ontology_client.py → ontology_clients.py} +17 -2
- dsp_tools/clients/{ontology_client_live.py → ontology_create_client_live.py} +21 -40
- dsp_tools/clients/ontology_get_client_live.py +66 -0
- dsp_tools/clients/project_client.py +10 -0
- dsp_tools/clients/project_client_live.py +36 -0
- dsp_tools/commands/create/create_on_server/cardinalities.py +14 -8
- dsp_tools/commands/create/create_on_server/lists.py +163 -0
- dsp_tools/commands/create/lists_only.py +45 -0
- dsp_tools/commands/create/models/input_problems.py +13 -0
- dsp_tools/commands/create/models/parsed_project.py +14 -1
- dsp_tools/commands/create/models/rdf_ontology.py +0 -7
- dsp_tools/commands/create/models/server_project_info.py +17 -3
- dsp_tools/commands/create/parsing/parse_lists.py +45 -0
- dsp_tools/commands/create/parsing/parse_project.py +23 -4
- dsp_tools/commands/ingest_xmlupload/create_resources/upload_xml.py +4 -4
- dsp_tools/commands/project/create/project_create_all.py +17 -13
- dsp_tools/commands/project/create/project_create_default_permissions.py +8 -6
- dsp_tools/commands/project/create/project_create_ontologies.py +30 -18
- dsp_tools/commands/project/legacy_models/listnode.py +0 -30
- dsp_tools/commands/validate_data/models/api_responses.py +2 -16
- dsp_tools/commands/validate_data/prepare_data/prepare_data.py +11 -10
- dsp_tools/commands/validate_data/shacl_cli_validator.py +3 -1
- dsp_tools/commands/validate_data/sparql/value_shacl.py +1 -1
- dsp_tools/commands/validate_data/validate_data.py +3 -3
- dsp_tools/commands/validate_data/validation/get_validation_report.py +1 -1
- dsp_tools/commands/validate_data/validation/validate_ontology.py +1 -1
- dsp_tools/commands/xmlupload/models/input_problems.py +1 -1
- dsp_tools/commands/xmlupload/upload_config.py +1 -1
- dsp_tools/commands/xmlupload/xmlupload.py +2 -2
- dsp_tools/error/custom_warnings.py +7 -0
- dsp_tools/error/exceptions.py +25 -2
- dsp_tools/resources/start-stack/docker-compose.yml +23 -23
- dsp_tools/utils/ansi_colors.py +2 -0
- dsp_tools/utils/fuseki_bloating.py +4 -2
- dsp_tools/utils/request_utils.py +31 -0
- dsp_tools/xmllib/models/res.py +2 -0
- {dsp_tools-17.0.0.post29.dist-info → dsp_tools-18.0.0.post3.dist-info}/METADATA +1 -1
- {dsp_tools-17.0.0.post29.dist-info → dsp_tools-18.0.0.post3.dist-info}/RECORD +48 -39
- {dsp_tools-17.0.0.post29.dist-info → dsp_tools-18.0.0.post3.dist-info}/WHEEL +1 -1
- dsp_tools/commands/project/create/project_create_lists.py +0 -200
- dsp_tools/commands/validate_data/api_clients.py +0 -124
- {dsp_tools-17.0.0.post29.dist-info → dsp_tools-18.0.0.post3.dist-info}/entry_points.txt +0 -0
|
@@ -1,22 +1,24 @@
|
|
|
1
1
|
from dataclasses import dataclass
|
|
2
2
|
from http import HTTPStatus
|
|
3
3
|
from typing import Any
|
|
4
|
+
from typing import cast
|
|
4
5
|
|
|
5
6
|
import requests
|
|
6
7
|
from loguru import logger
|
|
7
8
|
from rdflib import Graph
|
|
8
9
|
from rdflib import Literal
|
|
9
10
|
from rdflib import URIRef
|
|
10
|
-
from requests import
|
|
11
|
+
from requests import RequestException
|
|
11
12
|
from requests import Response
|
|
12
13
|
|
|
13
14
|
from dsp_tools.clients.authentication_client import AuthenticationClient
|
|
14
|
-
from dsp_tools.clients.
|
|
15
|
+
from dsp_tools.clients.ontology_clients import OntologyCreateClient
|
|
15
16
|
from dsp_tools.error.exceptions import BadCredentialsError
|
|
16
|
-
from dsp_tools.error.exceptions import
|
|
17
|
+
from dsp_tools.error.exceptions import FatalNonOkApiResponseCode
|
|
17
18
|
from dsp_tools.utils.rdflib_constants import KNORA_API
|
|
18
19
|
from dsp_tools.utils.request_utils import RequestParameters
|
|
19
|
-
from dsp_tools.utils.request_utils import
|
|
20
|
+
from dsp_tools.utils.request_utils import log_and_raise_request_exception
|
|
21
|
+
from dsp_tools.utils.request_utils import log_and_warn_unexpected_non_ok_response
|
|
20
22
|
from dsp_tools.utils.request_utils import log_request
|
|
21
23
|
from dsp_tools.utils.request_utils import log_response
|
|
22
24
|
|
|
@@ -24,7 +26,7 @@ TIMEOUT = 60
|
|
|
24
26
|
|
|
25
27
|
|
|
26
28
|
@dataclass
|
|
27
|
-
class
|
|
29
|
+
class OntologyCreateClientLive(OntologyCreateClient):
|
|
28
30
|
"""
|
|
29
31
|
Client for the ontology endpoint in the API.
|
|
30
32
|
"""
|
|
@@ -38,23 +40,12 @@ class OntologyClientLive(OntologyClient):
|
|
|
38
40
|
logger.debug("GET ontology metadata")
|
|
39
41
|
try:
|
|
40
42
|
response = self._get_and_log_request(url, header)
|
|
41
|
-
except
|
|
42
|
-
|
|
43
|
+
except RequestException as err:
|
|
44
|
+
log_and_raise_request_exception(err)
|
|
45
|
+
|
|
43
46
|
if response.ok:
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
raise UnexpectedApiResponseError(
|
|
47
|
-
f"Could not find the last modification date of the ontology '{onto_iri}' "
|
|
48
|
-
f"in the response: {response.text}"
|
|
49
|
-
)
|
|
50
|
-
return date
|
|
51
|
-
if response.status_code == HTTPStatus.FORBIDDEN:
|
|
52
|
-
raise BadCredentialsError("You do not have sufficient credentials to retrieve ontology metadata.")
|
|
53
|
-
else:
|
|
54
|
-
raise UnexpectedApiResponseError(
|
|
55
|
-
f"An unexpected response with the status code {response.status_code} was received from the API. "
|
|
56
|
-
f"Please consult 'warnings.log' for details."
|
|
57
|
-
)
|
|
47
|
+
return _parse_last_modification_date(response.text, URIRef(onto_iri))
|
|
48
|
+
raise FatalNonOkApiResponseCode(url, response.status_code, response.text)
|
|
58
49
|
|
|
59
50
|
def post_resource_cardinalities(self, cardinality_graph: dict[str, Any]) -> Literal | None:
|
|
60
51
|
url = f"{self.server}/v2/ontologies/cardinalities"
|
|
@@ -62,26 +53,18 @@ class OntologyClientLive(OntologyClient):
|
|
|
62
53
|
logger.debug("POST resource cardinalities to ontology")
|
|
63
54
|
try:
|
|
64
55
|
response = self._post_and_log_request(url, cardinality_graph)
|
|
65
|
-
except
|
|
66
|
-
|
|
56
|
+
except RequestException as err:
|
|
57
|
+
log_and_raise_request_exception(err)
|
|
58
|
+
|
|
67
59
|
if response.ok:
|
|
68
|
-
|
|
69
|
-
if not date:
|
|
70
|
-
raise UnexpectedApiResponseError(
|
|
71
|
-
f"Could not find the last modification date in the response: {response.text}"
|
|
72
|
-
)
|
|
73
|
-
return date
|
|
60
|
+
return _parse_last_modification_date(response.text)
|
|
74
61
|
if response.status_code == HTTPStatus.FORBIDDEN:
|
|
75
62
|
raise BadCredentialsError(
|
|
76
63
|
"Only a project or system administrator can add cardinalities to resource classes. "
|
|
77
64
|
"Your permissions are insufficient for this action."
|
|
78
65
|
)
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
f"During cardinality creation an unexpected response with the status code {response.status_code} "
|
|
82
|
-
f"was received from the API."
|
|
83
|
-
)
|
|
84
|
-
return None
|
|
66
|
+
log_and_warn_unexpected_non_ok_response(response.status_code, response.text)
|
|
67
|
+
return None
|
|
85
68
|
|
|
86
69
|
def _post_and_log_request(
|
|
87
70
|
self,
|
|
@@ -130,10 +113,8 @@ class OntologyClientLive(OntologyClient):
|
|
|
130
113
|
return data_dict, generic_headers
|
|
131
114
|
|
|
132
115
|
|
|
133
|
-
def _parse_last_modification_date(response_text: str, onto_iri: URIRef | None = None) -> Literal
|
|
116
|
+
def _parse_last_modification_date(response_text: str, onto_iri: URIRef | None = None) -> Literal:
|
|
134
117
|
g = Graph()
|
|
135
118
|
g.parse(data=response_text, format="json-ld")
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
return result
|
|
139
|
-
return None
|
|
119
|
+
date = next(g.objects(subject=onto_iri, predicate=KNORA_API.lastModificationDate))
|
|
120
|
+
return cast(Literal, date)
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from typing import Any
|
|
3
|
+
from typing import cast
|
|
4
|
+
|
|
5
|
+
import requests
|
|
6
|
+
|
|
7
|
+
from dsp_tools.clients.ontology_clients import OntologyGetClient
|
|
8
|
+
from dsp_tools.error.exceptions import FatalNonOkApiResponseCode
|
|
9
|
+
from dsp_tools.error.exceptions import InternalError
|
|
10
|
+
from dsp_tools.utils.request_utils import RequestParameters
|
|
11
|
+
from dsp_tools.utils.request_utils import log_request
|
|
12
|
+
from dsp_tools.utils.request_utils import log_response
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class OntologyGetClientLive(OntologyGetClient):
|
|
17
|
+
api_url: str
|
|
18
|
+
shortcode: str
|
|
19
|
+
|
|
20
|
+
def get_knora_api(self) -> str:
|
|
21
|
+
url = f"{self.api_url}/ontology/knora-api/v2#"
|
|
22
|
+
headers = {"Accept": "text/turtle"}
|
|
23
|
+
timeout = 60
|
|
24
|
+
log_request(RequestParameters("GET", url, timeout=timeout, headers=headers))
|
|
25
|
+
response = requests.get(url=url, headers=headers, timeout=timeout)
|
|
26
|
+
log_response(response, include_response_content=False)
|
|
27
|
+
if response.ok:
|
|
28
|
+
return response.text
|
|
29
|
+
raise FatalNonOkApiResponseCode(url, response.status_code, response.text)
|
|
30
|
+
|
|
31
|
+
def get_ontologies(self) -> tuple[list[str], list[str]]:
|
|
32
|
+
"""
|
|
33
|
+
Returns a list of project ontologies as a string in turtle format.
|
|
34
|
+
And a list of the ontology IRIs
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
list of ontologies and IRIs
|
|
38
|
+
"""
|
|
39
|
+
ontology_iris = self._get_ontology_iris()
|
|
40
|
+
ontologies = [self._get_one_ontology(x) for x in ontology_iris]
|
|
41
|
+
return ontologies, ontology_iris
|
|
42
|
+
|
|
43
|
+
def _get_ontology_iris(self) -> list[str]:
|
|
44
|
+
url = f"{self.api_url}/admin/projects/shortcode/{self.shortcode}"
|
|
45
|
+
timeout = 10
|
|
46
|
+
log_request(RequestParameters("GET", url, timeout=timeout))
|
|
47
|
+
response = requests.get(url=url, timeout=timeout)
|
|
48
|
+
log_response(response)
|
|
49
|
+
if not response.ok:
|
|
50
|
+
raise InternalError(f"Failed Request: {response.status_code} {response.text}")
|
|
51
|
+
response_json = cast(dict[str, Any], response.json())
|
|
52
|
+
if not (ontos := response_json.get("project", {}).get("ontologies")):
|
|
53
|
+
raise FatalNonOkApiResponseCode(url, response.status_code, response.text)
|
|
54
|
+
output = cast(list[str], ontos)
|
|
55
|
+
return output
|
|
56
|
+
|
|
57
|
+
def _get_one_ontology(self, ontology_iri: str) -> str:
|
|
58
|
+
url = ontology_iri
|
|
59
|
+
headers = {"Accept": "text/turtle"}
|
|
60
|
+
timeout = 30
|
|
61
|
+
log_request(RequestParameters("GET", url, timeout=timeout, headers=headers))
|
|
62
|
+
response = requests.get(url=url, headers=headers, timeout=timeout)
|
|
63
|
+
log_response(response, include_response_content=False)
|
|
64
|
+
if response.ok:
|
|
65
|
+
return response.text
|
|
66
|
+
raise FatalNonOkApiResponseCode(url, response.status_code, response.text)
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from http import HTTPStatus
|
|
3
|
+
from typing import cast
|
|
4
|
+
|
|
5
|
+
import requests
|
|
6
|
+
from requests import RequestException
|
|
7
|
+
|
|
8
|
+
from dsp_tools.clients.project_client import ProjectInfoClient
|
|
9
|
+
from dsp_tools.error.exceptions import FatalNonOkApiResponseCode
|
|
10
|
+
from dsp_tools.utils.request_utils import RequestParameters
|
|
11
|
+
from dsp_tools.utils.request_utils import log_and_raise_request_exception
|
|
12
|
+
from dsp_tools.utils.request_utils import log_request
|
|
13
|
+
from dsp_tools.utils.request_utils import log_response
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class ProjectInfoClientLive(ProjectInfoClient):
|
|
18
|
+
api_url: str
|
|
19
|
+
|
|
20
|
+
def get_project_iri(self, shortcode: str) -> str | None:
|
|
21
|
+
url = f"{self.api_url}/admin/projects/shortcode/{shortcode}"
|
|
22
|
+
timeout = 30
|
|
23
|
+
params = RequestParameters("GET", url, timeout)
|
|
24
|
+
log_request(params)
|
|
25
|
+
try:
|
|
26
|
+
response = requests.get(url, timeout=timeout)
|
|
27
|
+
except RequestException as err:
|
|
28
|
+
log_and_raise_request_exception(err)
|
|
29
|
+
|
|
30
|
+
log_response(response)
|
|
31
|
+
if response.ok:
|
|
32
|
+
result = response.json()
|
|
33
|
+
return cast(str, result["project"]["id"])
|
|
34
|
+
if response.status_code == HTTPStatus.NOT_FOUND:
|
|
35
|
+
return None
|
|
36
|
+
raise FatalNonOkApiResponseCode(url, response.status_code, response.text)
|
|
@@ -3,9 +3,10 @@ from typing import Any
|
|
|
3
3
|
from loguru import logger
|
|
4
4
|
from rdflib import Literal
|
|
5
5
|
from rdflib import URIRef
|
|
6
|
+
from tqdm import tqdm
|
|
6
7
|
|
|
7
|
-
from dsp_tools.clients.
|
|
8
|
-
from dsp_tools.clients.
|
|
8
|
+
from dsp_tools.clients.ontology_clients import OntologyCreateClient
|
|
9
|
+
from dsp_tools.clients.ontology_create_client_live import OntologyCreateClientLive
|
|
9
10
|
from dsp_tools.commands.create.models.input_problems import CollectedProblems
|
|
10
11
|
from dsp_tools.commands.create.models.input_problems import CreateProblem
|
|
11
12
|
from dsp_tools.commands.create.models.input_problems import ProblemType
|
|
@@ -25,7 +26,7 @@ def add_all_cardinalities(
|
|
|
25
26
|
ontologies: list[ParsedOntology],
|
|
26
27
|
project_iri_lookup: ProjectIriLookup,
|
|
27
28
|
created_iris: CreatedIriCollection,
|
|
28
|
-
onto_client:
|
|
29
|
+
onto_client: OntologyCreateClientLive,
|
|
29
30
|
) -> CollectedProblems | None:
|
|
30
31
|
all_problems = []
|
|
31
32
|
for onto in ontologies:
|
|
@@ -36,25 +37,30 @@ def add_all_cardinalities(
|
|
|
36
37
|
problems = _add_all_cardinalities_for_one_onto(
|
|
37
38
|
cardinalities=onto.cardinalities,
|
|
38
39
|
onto_iri=URIRef(onto_iri),
|
|
40
|
+
onto_name=onto.name,
|
|
39
41
|
last_modification_date=last_mod_date,
|
|
40
42
|
onto_client=onto_client,
|
|
41
43
|
created_iris=created_iris,
|
|
42
44
|
)
|
|
43
45
|
all_problems.extend(problems)
|
|
44
46
|
if all_problems:
|
|
45
|
-
return CollectedProblems("
|
|
47
|
+
return CollectedProblems(" While adding cardinalities the following problems occurred:", all_problems)
|
|
46
48
|
return None
|
|
47
49
|
|
|
48
50
|
|
|
49
51
|
def _add_all_cardinalities_for_one_onto(
|
|
50
52
|
cardinalities: list[ParsedClassCardinalities],
|
|
51
53
|
onto_iri: URIRef,
|
|
54
|
+
onto_name: str,
|
|
52
55
|
last_modification_date: Literal,
|
|
53
|
-
onto_client:
|
|
56
|
+
onto_client: OntologyCreateClient,
|
|
54
57
|
created_iris: CreatedIriCollection,
|
|
55
58
|
) -> list[CreateProblem]:
|
|
56
59
|
problems: list[CreateProblem] = []
|
|
57
|
-
|
|
60
|
+
progress_bar = tqdm(
|
|
61
|
+
cardinalities, desc=f" Adding cardinalities to the ontology '{onto_name}'", dynamic_ncols=True
|
|
62
|
+
)
|
|
63
|
+
for c in progress_bar:
|
|
58
64
|
# we do not inform about classes failures here, as it will have been done upstream
|
|
59
65
|
if c.class_iri not in created_iris.classes:
|
|
60
66
|
logger.warning(f"CARDINALITY: Class '{c.class_iri}' not in successes, no cardinalities added.")
|
|
@@ -74,7 +80,7 @@ def _add_cardinalities_for_one_class(
|
|
|
74
80
|
resource_card: ParsedClassCardinalities,
|
|
75
81
|
onto_iri: URIRef,
|
|
76
82
|
last_modification_date: Literal,
|
|
77
|
-
onto_client:
|
|
83
|
+
onto_client: OntologyCreateClient,
|
|
78
84
|
successful_props: set[str],
|
|
79
85
|
) -> tuple[Literal, list[UploadProblem]]:
|
|
80
86
|
res_iri = URIRef(resource_card.class_iri)
|
|
@@ -96,7 +102,7 @@ def _add_one_cardinality(
|
|
|
96
102
|
res_iri: URIRef,
|
|
97
103
|
onto_iri: URIRef,
|
|
98
104
|
last_modification_date: Literal,
|
|
99
|
-
onto_client:
|
|
105
|
+
onto_client: OntologyCreateClient,
|
|
100
106
|
) -> tuple[Literal, UploadProblem | None]:
|
|
101
107
|
card_serialised = _serialise_card(card, res_iri, onto_iri, last_modification_date)
|
|
102
108
|
new_mod_date = onto_client.post_resource_cardinalities(card_serialised)
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import warnings
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
from loguru import logger
|
|
5
|
+
from tqdm import tqdm
|
|
6
|
+
|
|
7
|
+
from dsp_tools.clients.authentication_client import AuthenticationClient
|
|
8
|
+
from dsp_tools.clients.list_client import ListCreateClient
|
|
9
|
+
from dsp_tools.clients.list_client_live import ListCreateClientLive
|
|
10
|
+
from dsp_tools.clients.list_client_live import ListGetClientLive
|
|
11
|
+
from dsp_tools.commands.create.models.input_problems import CollectedProblems
|
|
12
|
+
from dsp_tools.commands.create.models.input_problems import CreateProblem
|
|
13
|
+
from dsp_tools.commands.create.models.input_problems import ProblemType
|
|
14
|
+
from dsp_tools.commands.create.models.input_problems import UploadProblem
|
|
15
|
+
from dsp_tools.commands.create.models.input_problems import UserInformation
|
|
16
|
+
from dsp_tools.commands.create.models.input_problems import UserInformationMessage
|
|
17
|
+
from dsp_tools.commands.create.models.parsed_project import ParsedList
|
|
18
|
+
from dsp_tools.commands.create.models.parsed_project import ParsedListNode
|
|
19
|
+
from dsp_tools.commands.create.models.parsed_project import ParsedNodeInfo
|
|
20
|
+
from dsp_tools.commands.create.models.server_project_info import ListNameToIriLookup
|
|
21
|
+
from dsp_tools.error.custom_warnings import DspToolsUnexpectedStatusCodeWarning
|
|
22
|
+
from dsp_tools.error.exceptions import FatalNonOkApiResponseCode
|
|
23
|
+
from dsp_tools.utils.ansi_colors import BOLD
|
|
24
|
+
from dsp_tools.utils.ansi_colors import BOLD_CYAN
|
|
25
|
+
from dsp_tools.utils.ansi_colors import RESET_TO_DEFAULT
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def create_lists(
|
|
29
|
+
parsed_lists: list[ParsedList], shortcode: str, auth: AuthenticationClient, project_iri: str
|
|
30
|
+
) -> tuple[ListNameToIriLookup, CollectedProblems | None]:
|
|
31
|
+
print(BOLD + "Processing list section:" + RESET_TO_DEFAULT)
|
|
32
|
+
name2iri = get_existing_lists_on_server(shortcode, auth)
|
|
33
|
+
if not parsed_lists:
|
|
34
|
+
return name2iri, None
|
|
35
|
+
lists_to_create, existing_info = _filter_out_existing_lists(parsed_lists, name2iri)
|
|
36
|
+
if existing_info:
|
|
37
|
+
_print_existing_list_info(existing_info)
|
|
38
|
+
if not lists_to_create:
|
|
39
|
+
msg = " All lists defined in the project are already on the server, no list was uploaded."
|
|
40
|
+
logger.warning(msg)
|
|
41
|
+
print(BOLD_CYAN + msg + RESET_TO_DEFAULT)
|
|
42
|
+
return name2iri, None
|
|
43
|
+
|
|
44
|
+
create_client = ListCreateClientLive(auth.server, auth, project_iri)
|
|
45
|
+
|
|
46
|
+
all_problems: list[CreateProblem] = []
|
|
47
|
+
progress_bar = tqdm(lists_to_create, desc=" Creating lists", dynamic_ncols=True)
|
|
48
|
+
for new_lst in progress_bar:
|
|
49
|
+
list_iri, problems = _create_new_list(new_lst, create_client, project_iri)
|
|
50
|
+
if list_iri is None:
|
|
51
|
+
problems.extend(problems)
|
|
52
|
+
else:
|
|
53
|
+
name2iri.add_iri(new_lst.list_info.name, list_iri)
|
|
54
|
+
|
|
55
|
+
create_problems = None
|
|
56
|
+
if all_problems:
|
|
57
|
+
create_problems = CollectedProblems("The following problems occurred during list creation:", all_problems)
|
|
58
|
+
return name2iri, create_problems
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _print_existing_list_info(existing_lists: list[UserInformation]) -> None:
|
|
62
|
+
lists = ", ".join([x.focus_object for x in existing_lists])
|
|
63
|
+
msg = f" The following lists already exist on the server and will be skipped: {lists}"
|
|
64
|
+
logger.info(msg)
|
|
65
|
+
print(BOLD_CYAN + msg + RESET_TO_DEFAULT)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def get_existing_lists_on_server(shortcode: str, auth: AuthenticationClient) -> ListNameToIriLookup:
|
|
69
|
+
client = ListGetClientLive(auth.server, shortcode)
|
|
70
|
+
try:
|
|
71
|
+
name2iri_dict = client.get_all_list_iris_and_names()
|
|
72
|
+
return ListNameToIriLookup(name2iri_dict)
|
|
73
|
+
except FatalNonOkApiResponseCode as e:
|
|
74
|
+
logger.exception(e)
|
|
75
|
+
warnings.warn(
|
|
76
|
+
DspToolsUnexpectedStatusCodeWarning(
|
|
77
|
+
"Could not retrieve existing lists on server. "
|
|
78
|
+
"We will not be able to create any properties that require a list that is not defined in the JSON."
|
|
79
|
+
)
|
|
80
|
+
)
|
|
81
|
+
return ListNameToIriLookup({})
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _filter_out_existing_lists(
|
|
85
|
+
parsed_lists: list[ParsedList], name2iri: ListNameToIriLookup
|
|
86
|
+
) -> tuple[list[ParsedList], list[UserInformation]]:
|
|
87
|
+
lists_to_create: list[ParsedList] = []
|
|
88
|
+
existing_info: list[UserInformation] = []
|
|
89
|
+
|
|
90
|
+
for parsed_list in parsed_lists:
|
|
91
|
+
if name2iri.check_list_exists(parsed_list.list_info.name):
|
|
92
|
+
existing_info.append(
|
|
93
|
+
UserInformation(parsed_list.list_info.name, UserInformationMessage.LIST_EXISTS_ON_SERVER)
|
|
94
|
+
)
|
|
95
|
+
else:
|
|
96
|
+
lists_to_create.append(parsed_list)
|
|
97
|
+
|
|
98
|
+
return lists_to_create, existing_info
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _create_new_list(
|
|
102
|
+
parsed_list: ParsedList, create_client: ListCreateClient, project_iri: str
|
|
103
|
+
) -> tuple[str | None, list[UploadProblem]]:
|
|
104
|
+
serialised = _serialise_list(parsed_list.list_info, project_iri)
|
|
105
|
+
new_iri = create_client.create_new_list(serialised)
|
|
106
|
+
|
|
107
|
+
if new_iri is None:
|
|
108
|
+
problems: list[UploadProblem] = [
|
|
109
|
+
UploadProblem(parsed_list.list_info.name, ProblemType.LIST_COULD_NOT_BE_CREATED)
|
|
110
|
+
]
|
|
111
|
+
return None, problems
|
|
112
|
+
|
|
113
|
+
node_problems = []
|
|
114
|
+
if parsed_list.children:
|
|
115
|
+
node_problems = _create_node_tree(parsed_list.children, new_iri, create_client, project_iri)
|
|
116
|
+
|
|
117
|
+
return new_iri, node_problems
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _serialise_list(parsed_list_info: ParsedNodeInfo, project_iri: str) -> dict[str, Any]:
|
|
121
|
+
node_dict = {
|
|
122
|
+
"projectIri": project_iri,
|
|
123
|
+
"name": parsed_list_info.name,
|
|
124
|
+
"labels": _convert_to_api_format(parsed_list_info.labels),
|
|
125
|
+
}
|
|
126
|
+
if parsed_list_info.comments:
|
|
127
|
+
node_dict["comments"] = _convert_to_api_format(parsed_list_info.comments)
|
|
128
|
+
return node_dict
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _create_node_tree(
|
|
132
|
+
nodes: list[ParsedListNode], parent_iri: str, create_client: ListCreateClient, project_iri: str
|
|
133
|
+
) -> list[UploadProblem]:
|
|
134
|
+
problems: list[UploadProblem] = []
|
|
135
|
+
|
|
136
|
+
for node in nodes:
|
|
137
|
+
serialised = _serialise_node(node.node_info, parent_iri, project_iri)
|
|
138
|
+
node_iri = create_client.add_list_node(serialised, parent_iri)
|
|
139
|
+
if node_iri is None:
|
|
140
|
+
problems.append(UploadProblem(node.node_info.name, ProblemType.LIST_NODE_COULD_NOT_BE_CREATED))
|
|
141
|
+
continue
|
|
142
|
+
|
|
143
|
+
if node.children:
|
|
144
|
+
child_problems = _create_node_tree(node.children, node_iri, create_client, project_iri)
|
|
145
|
+
problems.extend(child_problems)
|
|
146
|
+
|
|
147
|
+
return problems
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _serialise_node(node_info: ParsedNodeInfo, parent_iri: str, project_iri: str) -> dict[str, Any]:
|
|
151
|
+
node_dict = {
|
|
152
|
+
"parentNodeIri": parent_iri,
|
|
153
|
+
"projectIri": project_iri,
|
|
154
|
+
"name": node_info.name,
|
|
155
|
+
"labels": _convert_to_api_format(node_info.labels),
|
|
156
|
+
}
|
|
157
|
+
if node_info.comments:
|
|
158
|
+
node_dict["comments"] = _convert_to_api_format(node_info.comments)
|
|
159
|
+
return node_dict
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _convert_to_api_format(lang_dict: dict[str, str]) -> list[dict[str, str]]:
|
|
163
|
+
return [{"value": value, "language": lang} for lang, value in lang_dict.items()]
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
from loguru import logger
|
|
5
|
+
|
|
6
|
+
from dsp_tools.cli.args import ServerCredentials
|
|
7
|
+
from dsp_tools.clients.authentication_client_live import AuthenticationClientLive
|
|
8
|
+
from dsp_tools.clients.project_client_live import ProjectInfoClientLive
|
|
9
|
+
from dsp_tools.commands.create.communicate_problems import print_problem_collection
|
|
10
|
+
from dsp_tools.commands.create.create_on_server.lists import create_lists
|
|
11
|
+
from dsp_tools.commands.create.models.input_problems import CollectedProblems
|
|
12
|
+
from dsp_tools.commands.create.parsing.parse_project import parse_lists_only
|
|
13
|
+
from dsp_tools.error.exceptions import ProjectNotFoundError
|
|
14
|
+
from dsp_tools.utils.ansi_colors import BACKGROUND_BOLD_YELLOW
|
|
15
|
+
from dsp_tools.utils.ansi_colors import RESET_TO_DEFAULT
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def create_lists_only(project_file_as_path_or_parsed: str | Path | dict[str, Any], creds: ServerCredentials) -> bool:
|
|
19
|
+
result = parse_lists_only(project_file_as_path_or_parsed)
|
|
20
|
+
if isinstance(result, CollectedProblems):
|
|
21
|
+
print_problem_collection(result)
|
|
22
|
+
return False
|
|
23
|
+
project_metadata, parsed_lists = result
|
|
24
|
+
|
|
25
|
+
if not parsed_lists:
|
|
26
|
+
msg = "Your file did not contain any lists, therefore no lists were created on the server."
|
|
27
|
+
logger.info(msg)
|
|
28
|
+
print(BACKGROUND_BOLD_YELLOW + msg + RESET_TO_DEFAULT)
|
|
29
|
+
return True
|
|
30
|
+
|
|
31
|
+
project_info = ProjectInfoClientLive(creds.server)
|
|
32
|
+
project_iri = project_info.get_project_iri(project_metadata.shortcode)
|
|
33
|
+
if not project_iri:
|
|
34
|
+
raise ProjectNotFoundError(
|
|
35
|
+
f"This commands adds lists to an existing project. "
|
|
36
|
+
f"The project with the shortcode {project_metadata.shortcode} does not exist on this server. "
|
|
37
|
+
f"If you wish to create an entire project use the `create` command without the flag."
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
auth = AuthenticationClientLive(creds.server, creds.user, creds.password)
|
|
41
|
+
_, problems = create_lists(parsed_lists, project_metadata.shortcode, auth, project_iri)
|
|
42
|
+
if problems:
|
|
43
|
+
print_problem_collection(problems)
|
|
44
|
+
return False
|
|
45
|
+
return True
|
|
@@ -10,6 +10,16 @@ class CollectedProblems:
|
|
|
10
10
|
problems: list[CreateProblem]
|
|
11
11
|
|
|
12
12
|
|
|
13
|
+
@dataclass
|
|
14
|
+
class UserInformation:
|
|
15
|
+
focus_object: str
|
|
16
|
+
msg: UserInformationMessage
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class UserInformationMessage(StrEnum):
|
|
20
|
+
LIST_EXISTS_ON_SERVER = "The list already exists on the server, therefore it is skipped entirely."
|
|
21
|
+
|
|
22
|
+
|
|
13
23
|
@dataclass
|
|
14
24
|
class CreateProblem:
|
|
15
25
|
problematic_object: str
|
|
@@ -30,3 +40,6 @@ class ProblemType(StrEnum):
|
|
|
30
40
|
"nor does it belong to one of the project ontologies."
|
|
31
41
|
)
|
|
32
42
|
CARDINALITY_COULD_NOT_BE_ADDED = "The cardinality could not be added."
|
|
43
|
+
DUPLICATE_LIST_NAME = "You have lists in your project with the same name, this is not permitted."
|
|
44
|
+
LIST_COULD_NOT_BE_CREATED = "The list could not be created on the server."
|
|
45
|
+
LIST_NODE_COULD_NOT_BE_CREATED = "The list node could not be created on the server."
|
|
@@ -45,5 +45,18 @@ class ParsedUser:
|
|
|
45
45
|
|
|
46
46
|
@dataclass
|
|
47
47
|
class ParsedList:
|
|
48
|
+
list_info: ParsedNodeInfo
|
|
49
|
+
children: list[ParsedListNode]
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@dataclass
|
|
53
|
+
class ParsedNodeInfo:
|
|
48
54
|
name: str
|
|
49
|
-
|
|
55
|
+
labels: dict[str, str]
|
|
56
|
+
comments: dict[str, str] | None
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@dataclass
|
|
60
|
+
class ParsedListNode:
|
|
61
|
+
node_info: ParsedNodeInfo
|
|
62
|
+
children: list[ParsedListNode]
|
|
@@ -6,13 +6,6 @@ from rdflib import Literal
|
|
|
6
6
|
from rdflib import URIRef
|
|
7
7
|
|
|
8
8
|
|
|
9
|
-
@dataclass
|
|
10
|
-
class RdfResourceCardinality:
|
|
11
|
-
resource_iri: URIRef
|
|
12
|
-
on_property: URIRef
|
|
13
|
-
cardinality: RdfCardinalityRestriction
|
|
14
|
-
|
|
15
|
-
|
|
16
9
|
@dataclass
|
|
17
10
|
class RdfCardinalityRestriction:
|
|
18
11
|
owl_property: URIRef
|
|
@@ -2,6 +2,7 @@ from dataclasses import dataclass
|
|
|
2
2
|
from dataclasses import field
|
|
3
3
|
|
|
4
4
|
from dsp_tools.commands.create.constants import KNORA_API_STR
|
|
5
|
+
from dsp_tools.error.exceptions import InternalError
|
|
5
6
|
|
|
6
7
|
|
|
7
8
|
@dataclass
|
|
@@ -12,9 +13,6 @@ class ProjectIriLookup:
|
|
|
12
13
|
def add_onto(self, name: str, iri: str) -> None:
|
|
13
14
|
self.onto_iris[name] = iri
|
|
14
15
|
|
|
15
|
-
def get_onto_iri(self, name: str) -> str | None:
|
|
16
|
-
return self.onto_iris.get(name)
|
|
17
|
-
|
|
18
16
|
|
|
19
17
|
@dataclass
|
|
20
18
|
class CreatedIriCollection:
|
|
@@ -23,3 +21,19 @@ class CreatedIriCollection:
|
|
|
23
21
|
|
|
24
22
|
def __post_init__(self) -> None:
|
|
25
23
|
self.properties.update({f"{KNORA_API_STR}seqnum", f"{KNORA_API_STR}isPartOf"})
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass
|
|
27
|
+
class ListNameToIriLookup:
|
|
28
|
+
name2iri: dict[str, str]
|
|
29
|
+
|
|
30
|
+
def check_list_exists(self, name: str) -> bool:
|
|
31
|
+
return name in self.name2iri.keys()
|
|
32
|
+
|
|
33
|
+
def add_iri(self, name: str, iri: str) -> None:
|
|
34
|
+
if self.check_list_exists(name):
|
|
35
|
+
raise InternalError(f"List with the name '{name}' already exists in the lookup.")
|
|
36
|
+
self.name2iri[name] = iri
|
|
37
|
+
|
|
38
|
+
def get_iri(self, name: str) -> str | None:
|
|
39
|
+
return self.name2iri.get(name)
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
from collections import Counter
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
from dsp_tools.commands.create.models.input_problems import CollectedProblems
|
|
5
|
+
from dsp_tools.commands.create.models.input_problems import CreateProblem
|
|
6
|
+
from dsp_tools.commands.create.models.input_problems import InputProblem
|
|
7
|
+
from dsp_tools.commands.create.models.input_problems import ProblemType
|
|
8
|
+
from dsp_tools.commands.create.models.parsed_project import ParsedList
|
|
9
|
+
from dsp_tools.commands.create.models.parsed_project import ParsedListNode
|
|
10
|
+
from dsp_tools.commands.create.models.parsed_project import ParsedNodeInfo
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def parse_list_section(lists: list[dict[str, Any]]) -> list[ParsedList] | CollectedProblems:
|
|
14
|
+
list_section = [_parse_one_list(one_list) for one_list in lists]
|
|
15
|
+
list_names = [parsed_list.list_info.name for parsed_list in list_section]
|
|
16
|
+
duplicates = {name for name, count in Counter(list_names).items() if count > 1}
|
|
17
|
+
if duplicates:
|
|
18
|
+
problems: list[CreateProblem] = [
|
|
19
|
+
InputProblem(problematic_object=name, problem=ProblemType.DUPLICATE_LIST_NAME) for name in duplicates
|
|
20
|
+
]
|
|
21
|
+
return CollectedProblems(header="The following problems were found in the list section:", problems=problems)
|
|
22
|
+
return list_section
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _parse_one_list(one_list: dict[str, Any]) -> ParsedList:
|
|
26
|
+
list_info = _parse_node_info(one_list)
|
|
27
|
+
children = _parse_nodes_and_children(one_list["nodes"]) if "nodes" in one_list else []
|
|
28
|
+
return ParsedList(list_info=list_info, children=children)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _parse_node_info(one_node: dict[str, Any]) -> ParsedNodeInfo:
|
|
32
|
+
return ParsedNodeInfo(
|
|
33
|
+
name=one_node["name"],
|
|
34
|
+
labels=one_node["labels"],
|
|
35
|
+
comments=one_node.get("comments"),
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _parse_nodes_and_children(list_nodes: list[dict[str, Any]]) -> list[ParsedListNode]:
|
|
40
|
+
parsed_nodes = []
|
|
41
|
+
for node in list_nodes:
|
|
42
|
+
node_info = _parse_node_info(node)
|
|
43
|
+
children = _parse_nodes_and_children(node["nodes"]) if "nodes" in node else []
|
|
44
|
+
parsed_nodes.append(ParsedListNode(node_info=node_info, children=children))
|
|
45
|
+
return parsed_nodes
|