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.

Files changed (50) hide show
  1. dsp_tools/cli/args.py +13 -0
  2. dsp_tools/cli/call_action.py +34 -330
  3. dsp_tools/cli/call_action_files_only.py +74 -0
  4. dsp_tools/cli/call_action_with_network.py +202 -0
  5. dsp_tools/cli/create_parsers.py +53 -14
  6. dsp_tools/cli/utils.py +87 -0
  7. dsp_tools/clients/list_client.py +49 -0
  8. dsp_tools/clients/list_client_live.py +166 -0
  9. dsp_tools/clients/{ontology_client.py → ontology_clients.py} +17 -2
  10. dsp_tools/clients/{ontology_client_live.py → ontology_create_client_live.py} +21 -40
  11. dsp_tools/clients/ontology_get_client_live.py +66 -0
  12. dsp_tools/clients/project_client.py +10 -0
  13. dsp_tools/clients/project_client_live.py +36 -0
  14. dsp_tools/commands/create/create_on_server/cardinalities.py +14 -8
  15. dsp_tools/commands/create/create_on_server/lists.py +163 -0
  16. dsp_tools/commands/create/lists_only.py +45 -0
  17. dsp_tools/commands/create/models/input_problems.py +13 -0
  18. dsp_tools/commands/create/models/parsed_project.py +14 -1
  19. dsp_tools/commands/create/models/rdf_ontology.py +0 -7
  20. dsp_tools/commands/create/models/server_project_info.py +17 -3
  21. dsp_tools/commands/create/parsing/parse_lists.py +45 -0
  22. dsp_tools/commands/create/parsing/parse_project.py +23 -4
  23. dsp_tools/commands/ingest_xmlupload/create_resources/upload_xml.py +4 -4
  24. dsp_tools/commands/project/create/project_create_all.py +17 -13
  25. dsp_tools/commands/project/create/project_create_default_permissions.py +8 -6
  26. dsp_tools/commands/project/create/project_create_ontologies.py +30 -18
  27. dsp_tools/commands/project/legacy_models/listnode.py +0 -30
  28. dsp_tools/commands/validate_data/models/api_responses.py +2 -16
  29. dsp_tools/commands/validate_data/prepare_data/prepare_data.py +11 -10
  30. dsp_tools/commands/validate_data/shacl_cli_validator.py +3 -1
  31. dsp_tools/commands/validate_data/sparql/value_shacl.py +1 -1
  32. dsp_tools/commands/validate_data/validate_data.py +3 -3
  33. dsp_tools/commands/validate_data/validation/get_validation_report.py +1 -1
  34. dsp_tools/commands/validate_data/validation/validate_ontology.py +1 -1
  35. dsp_tools/commands/xmlupload/models/input_problems.py +1 -1
  36. dsp_tools/commands/xmlupload/upload_config.py +1 -1
  37. dsp_tools/commands/xmlupload/xmlupload.py +2 -2
  38. dsp_tools/error/custom_warnings.py +7 -0
  39. dsp_tools/error/exceptions.py +25 -2
  40. dsp_tools/resources/start-stack/docker-compose.yml +23 -23
  41. dsp_tools/utils/ansi_colors.py +2 -0
  42. dsp_tools/utils/fuseki_bloating.py +4 -2
  43. dsp_tools/utils/request_utils.py +31 -0
  44. dsp_tools/xmllib/models/res.py +2 -0
  45. {dsp_tools-17.0.0.post29.dist-info → dsp_tools-18.0.0.post3.dist-info}/METADATA +1 -1
  46. {dsp_tools-17.0.0.post29.dist-info → dsp_tools-18.0.0.post3.dist-info}/RECORD +48 -39
  47. {dsp_tools-17.0.0.post29.dist-info → dsp_tools-18.0.0.post3.dist-info}/WHEEL +1 -1
  48. dsp_tools/commands/project/create/project_create_lists.py +0 -200
  49. dsp_tools/commands/validate_data/api_clients.py +0 -124
  50. {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 ReadTimeout
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.ontology_client import OntologyClient
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 UnexpectedApiResponseError
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 log_and_raise_timeouts
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 OntologyClientLive(OntologyClient):
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 (TimeoutError, ReadTimeout) as err:
42
- log_and_raise_timeouts(err)
43
+ except RequestException as err:
44
+ log_and_raise_request_exception(err)
45
+
43
46
  if response.ok:
44
- date = _parse_last_modification_date(response.text, URIRef(onto_iri))
45
- if not date:
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 (TimeoutError, ReadTimeout) as err:
66
- log_and_raise_timeouts(err)
56
+ except RequestException as err:
57
+ log_and_raise_request_exception(err)
58
+
67
59
  if response.ok:
68
- date = _parse_last_modification_date(response.text)
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
- else:
80
- logger.error(
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 | None:
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
- result = next(g.objects(subject=onto_iri, predicate=KNORA_API.lastModificationDate), None)
137
- if isinstance(result, Literal):
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,10 @@
1
+ from dataclasses import dataclass
2
+ from typing import Protocol
3
+
4
+
5
+ @dataclass
6
+ class ProjectInfoClient(Protocol):
7
+ api_url: str
8
+
9
+ def get_project_iri(self, shortcode: str) -> str | None:
10
+ """Get the IRI of a project via shortcode."""
@@ -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.ontology_client import OntologyClient
8
- from dsp_tools.clients.ontology_client_live import OntologyClientLive
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: OntologyClientLive,
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("During the cardinality creation the following problems occurred:", all_problems)
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: OntologyClient,
56
+ onto_client: OntologyCreateClient,
54
57
  created_iris: CreatedIriCollection,
55
58
  ) -> list[CreateProblem]:
56
59
  problems: list[CreateProblem] = []
57
- for c in cardinalities:
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: OntologyClient,
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: OntologyClient,
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
- info: dict[str, Any]
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