dsp-tools 17.0.0.post26__py3-none-any.whl → 18.0.0__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 (46) hide show
  1. dsp_tools/cli/args.py +14 -0
  2. dsp_tools/cli/call_action.py +34 -327
  3. dsp_tools/cli/call_action_files_only.py +74 -0
  4. dsp_tools/cli/call_action_with_network.py +203 -0
  5. dsp_tools/cli/create_parsers.py +71 -11
  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 +157 -0
  9. dsp_tools/clients/metadata_client.py +4 -4
  10. dsp_tools/clients/metadata_client_live.py +6 -5
  11. dsp_tools/clients/{ontology_client.py → ontology_clients.py} +17 -2
  12. dsp_tools/clients/{ontology_client_live.py → ontology_create_client_live.py} +2 -2
  13. dsp_tools/clients/ontology_get_client_live.py +65 -0
  14. dsp_tools/clients/project_client.py +10 -0
  15. dsp_tools/clients/project_client_live.py +30 -0
  16. dsp_tools/commands/create/create_on_server/cardinalities.py +14 -8
  17. dsp_tools/commands/create/create_on_server/lists.py +150 -0
  18. dsp_tools/commands/create/lists_only.py +45 -0
  19. dsp_tools/commands/create/models/input_problems.py +13 -0
  20. dsp_tools/commands/create/models/parsed_project.py +14 -1
  21. dsp_tools/commands/create/models/rdf_ontology.py +0 -7
  22. dsp_tools/commands/create/models/server_project_info.py +17 -3
  23. dsp_tools/commands/create/parsing/parse_lists.py +45 -0
  24. dsp_tools/commands/create/parsing/parse_project.py +23 -4
  25. dsp_tools/commands/ingest_xmlupload/create_resources/upload_xml.py +4 -0
  26. dsp_tools/commands/project/create/project_create_all.py +17 -13
  27. dsp_tools/commands/project/create/project_create_default_permissions.py +8 -6
  28. dsp_tools/commands/project/create/project_create_ontologies.py +30 -18
  29. dsp_tools/commands/project/legacy_models/listnode.py +0 -30
  30. dsp_tools/commands/validate_data/models/api_responses.py +2 -16
  31. dsp_tools/commands/validate_data/models/input_problems.py +1 -0
  32. dsp_tools/commands/validate_data/prepare_data/prepare_data.py +24 -16
  33. dsp_tools/commands/validate_data/process_validation_report/get_user_validation_message.py +53 -23
  34. dsp_tools/commands/validate_data/sparql/value_shacl.py +1 -1
  35. dsp_tools/commands/validate_data/validate_data.py +11 -3
  36. dsp_tools/commands/xmlupload/upload_config.py +1 -0
  37. dsp_tools/commands/xmlupload/xmlupload.py +1 -0
  38. dsp_tools/error/exceptions.py +8 -0
  39. dsp_tools/resources/start-stack/docker-compose.yml +23 -23
  40. dsp_tools/utils/ansi_colors.py +2 -0
  41. {dsp_tools-17.0.0.post26.dist-info → dsp_tools-18.0.0.dist-info}/METADATA +1 -1
  42. {dsp_tools-17.0.0.post26.dist-info → dsp_tools-18.0.0.dist-info}/RECORD +44 -35
  43. {dsp_tools-17.0.0.post26.dist-info → dsp_tools-18.0.0.dist-info}/WHEEL +1 -1
  44. dsp_tools/commands/project/create/project_create_lists.py +0 -200
  45. dsp_tools/commands/validate_data/api_clients.py +0 -124
  46. {dsp_tools-17.0.0.post26.dist-info → dsp_tools-18.0.0.dist-info}/entry_points.txt +0 -0
@@ -6,9 +6,9 @@ from rdflib import Literal
6
6
  from dsp_tools.clients.authentication_client import AuthenticationClient
7
7
 
8
8
 
9
- class OntologyClient(Protocol):
9
+ class OntologyCreateClient(Protocol):
10
10
  """
11
- Protocol class/interface for the ontology endpoint in the API.
11
+ Protocol class/interface to create / update the ontology through the API.
12
12
  """
13
13
 
14
14
  server: str
@@ -19,3 +19,18 @@ class OntologyClient(Protocol):
19
19
 
20
20
  def post_resource_cardinalities(self, cardinality_graph: dict[str, Any]) -> Literal | None:
21
21
  """Add cardinalities to an existing resource class."""
22
+
23
+
24
+ class OntologyGetClient(Protocol):
25
+ """
26
+ Protocol class/interface to get ontologies from the API.
27
+ """
28
+
29
+ api_url: str
30
+ shortcode: str
31
+
32
+ def get_knora_api(self) -> str:
33
+ """Get the knora-api ontology."""
34
+
35
+ def get_ontologies(self) -> tuple[list[str], list[str]]:
36
+ """Get all project ontologies."""
@@ -11,7 +11,7 @@ from requests import ReadTimeout
11
11
  from requests import Response
12
12
 
13
13
  from dsp_tools.clients.authentication_client import AuthenticationClient
14
- from dsp_tools.clients.ontology_client import OntologyClient
14
+ from dsp_tools.clients.ontology_clients import OntologyCreateClient
15
15
  from dsp_tools.error.exceptions import BadCredentialsError
16
16
  from dsp_tools.error.exceptions import UnexpectedApiResponseError
17
17
  from dsp_tools.utils.rdflib_constants import KNORA_API
@@ -24,7 +24,7 @@ TIMEOUT = 60
24
24
 
25
25
 
26
26
  @dataclass
27
- class OntologyClientLive(OntologyClient):
27
+ class OntologyCreateClientLive(OntologyCreateClient):
28
28
  """
29
29
  Client for the ontology endpoint in the API.
30
30
  """
@@ -0,0 +1,65 @@
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 InternalError
9
+ from dsp_tools.utils.request_utils import RequestParameters
10
+ from dsp_tools.utils.request_utils import log_request
11
+ from dsp_tools.utils.request_utils import log_response
12
+
13
+
14
+ @dataclass
15
+ class OntologyGetClientLive(OntologyGetClient):
16
+ api_url: str
17
+ shortcode: str
18
+
19
+ def get_knora_api(self) -> str:
20
+ url = f"{self.api_url}/ontology/knora-api/v2#"
21
+ headers = {"Accept": "text/turtle"}
22
+ timeout = 60
23
+ log_request(RequestParameters("GET", url, timeout=timeout, headers=headers))
24
+ response = requests.get(url=url, headers=headers, timeout=timeout)
25
+ log_response(response, include_response_content=False)
26
+ if not response.ok:
27
+ raise InternalError(f"Failed Request: {response.status_code} {response.text}")
28
+ return response.text
29
+
30
+ def get_ontologies(self) -> tuple[list[str], list[str]]:
31
+ """
32
+ Returns a list of project ontologies as a string in turtle format.
33
+ And a list of the ontology IRIs
34
+
35
+ Returns:
36
+ list of ontologies and IRIs
37
+ """
38
+ ontology_iris = self._get_ontology_iris()
39
+ ontologies = [self._get_one_ontology(x) for x in ontology_iris]
40
+ return ontologies, ontology_iris
41
+
42
+ def _get_ontology_iris(self) -> list[str]:
43
+ url = f"{self.api_url}/admin/projects/shortcode/{self.shortcode}"
44
+ timeout = 10
45
+ log_request(RequestParameters("GET", url, timeout=timeout))
46
+ response = requests.get(url=url, timeout=timeout)
47
+ log_response(response)
48
+ if not response.ok:
49
+ raise InternalError(f"Failed Request: {response.status_code} {response.text}")
50
+ response_json = cast(dict[str, Any], response.json())
51
+ if not (ontos := response_json.get("project", {}).get("ontologies")):
52
+ raise InternalError(f"The response from the API does not contain any ontologies.\nResponse:{response.text}")
53
+ output = cast(list[str], ontos)
54
+ return output
55
+
56
+ def _get_one_ontology(self, ontology_iri: str) -> str:
57
+ url = ontology_iri
58
+ headers = {"Accept": "text/turtle"}
59
+ timeout = 30
60
+ log_request(RequestParameters("GET", url, timeout=timeout, headers=headers))
61
+ response = requests.get(url=url, headers=headers, timeout=timeout)
62
+ log_response(response, include_response_content=False)
63
+ if not response.ok:
64
+ raise InternalError(f"Failed Request: {response.status_code} {response.text}")
65
+ return 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,30 @@
1
+ from dataclasses import dataclass
2
+ from http import HTTPStatus
3
+ from typing import cast
4
+
5
+ import requests
6
+
7
+ from dsp_tools.clients.project_client import ProjectInfoClient
8
+ from dsp_tools.error.exceptions import UnexpectedApiResponseError
9
+ from dsp_tools.utils.request_utils import RequestParameters
10
+ from dsp_tools.utils.request_utils import log_request
11
+ from dsp_tools.utils.request_utils import log_response
12
+
13
+
14
+ @dataclass
15
+ class ProjectInfoClientLive(ProjectInfoClient):
16
+ api_url: str
17
+
18
+ def get_project_iri(self, shortcode: str) -> str | None:
19
+ url = f"{self.api_url}/admin/projects/shortcode/{shortcode}"
20
+ timeout = 30
21
+ params = RequestParameters("GET", url, timeout)
22
+ log_request(params)
23
+ response = requests.get(url, timeout=timeout)
24
+ log_response(response)
25
+ if response.ok:
26
+ result = response.json()
27
+ return cast(str, result["project"]["id"])
28
+ if response.status_code == HTTPStatus.NOT_FOUND:
29
+ return None
30
+ raise UnexpectedApiResponseError(f"Encountered unexpected API response with the code {response.status_code}")
@@ -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,150 @@
1
+ from typing import Any
2
+
3
+ from loguru import logger
4
+ from tqdm import tqdm
5
+
6
+ from dsp_tools.clients.authentication_client import AuthenticationClient
7
+ from dsp_tools.clients.list_client import ListCreateClient
8
+ from dsp_tools.clients.list_client_live import ListCreateClientLive
9
+ from dsp_tools.clients.list_client_live import ListGetClientLive
10
+ from dsp_tools.commands.create.models.input_problems import CollectedProblems
11
+ from dsp_tools.commands.create.models.input_problems import CreateProblem
12
+ from dsp_tools.commands.create.models.input_problems import ProblemType
13
+ from dsp_tools.commands.create.models.input_problems import UploadProblem
14
+ from dsp_tools.commands.create.models.input_problems import UserInformation
15
+ from dsp_tools.commands.create.models.input_problems import UserInformationMessage
16
+ from dsp_tools.commands.create.models.parsed_project import ParsedList
17
+ from dsp_tools.commands.create.models.parsed_project import ParsedListNode
18
+ from dsp_tools.commands.create.models.parsed_project import ParsedNodeInfo
19
+ from dsp_tools.commands.create.models.server_project_info import ListNameToIriLookup
20
+ from dsp_tools.utils.ansi_colors import BOLD
21
+ from dsp_tools.utils.ansi_colors import BOLD_CYAN
22
+ from dsp_tools.utils.ansi_colors import RESET_TO_DEFAULT
23
+
24
+
25
+ def create_lists(
26
+ parsed_lists: list[ParsedList], shortcode: str, auth: AuthenticationClient, project_iri: str
27
+ ) -> tuple[ListNameToIriLookup, CollectedProblems | None]:
28
+ print(BOLD + "Processing list section:" + RESET_TO_DEFAULT)
29
+ name2iri = get_existing_lists_on_server(shortcode, auth)
30
+ if not parsed_lists:
31
+ return name2iri, None
32
+ lists_to_create, existing_info = _filter_out_existing_lists(parsed_lists, name2iri)
33
+ if existing_info:
34
+ _print_existing_list_info(existing_info)
35
+ if not lists_to_create:
36
+ msg = " All lists defined in the project are already on the server, no list was uploaded."
37
+ logger.warning(msg)
38
+ print(BOLD_CYAN + msg + RESET_TO_DEFAULT)
39
+ return name2iri, None
40
+
41
+ create_client = ListCreateClientLive(auth.server, auth, project_iri)
42
+
43
+ all_problems: list[CreateProblem] = []
44
+ progress_bar = tqdm(lists_to_create, desc=" Creating lists", dynamic_ncols=True)
45
+ for new_lst in progress_bar:
46
+ list_iri, problems = _create_new_list(new_lst, create_client, project_iri)
47
+ if list_iri is None:
48
+ problems.extend(problems)
49
+ else:
50
+ name2iri.add_iri(new_lst.list_info.name, list_iri)
51
+
52
+ create_problems = None
53
+ if all_problems:
54
+ create_problems = CollectedProblems("The following problems occurred during list creation:", all_problems)
55
+ return name2iri, create_problems
56
+
57
+
58
+ def _print_existing_list_info(existing_lists: list[UserInformation]) -> None:
59
+ lists = ", ".join([x.focus_object for x in existing_lists])
60
+ msg = f" The following lists already exist on the server and will be skipped: {lists}"
61
+ logger.info(msg)
62
+ print(BOLD_CYAN + msg + RESET_TO_DEFAULT)
63
+
64
+
65
+ def get_existing_lists_on_server(shortcode: str, auth: AuthenticationClient) -> ListNameToIriLookup:
66
+ client = ListGetClientLive(auth.server, shortcode)
67
+ name2iri_dict = client.get_all_list_iris_and_names()
68
+ return ListNameToIriLookup(name2iri_dict)
69
+
70
+
71
+ def _filter_out_existing_lists(
72
+ parsed_lists: list[ParsedList], name2iri: ListNameToIriLookup
73
+ ) -> tuple[list[ParsedList], list[UserInformation]]:
74
+ lists_to_create: list[ParsedList] = []
75
+ existing_info: list[UserInformation] = []
76
+
77
+ for parsed_list in parsed_lists:
78
+ if name2iri.check_list_exists(parsed_list.list_info.name):
79
+ existing_info.append(
80
+ UserInformation(parsed_list.list_info.name, UserInformationMessage.LIST_EXISTS_ON_SERVER)
81
+ )
82
+ else:
83
+ lists_to_create.append(parsed_list)
84
+
85
+ return lists_to_create, existing_info
86
+
87
+
88
+ def _create_new_list(
89
+ parsed_list: ParsedList, create_client: ListCreateClient, project_iri: str
90
+ ) -> tuple[str | None, list[UploadProblem]]:
91
+ serialised = _serialise_list(parsed_list.list_info, project_iri)
92
+ new_iri = create_client.create_new_list(serialised)
93
+
94
+ if new_iri is None:
95
+ problems: list[UploadProblem] = [
96
+ UploadProblem(parsed_list.list_info.name, ProblemType.LIST_COULD_NOT_BE_CREATED)
97
+ ]
98
+ return None, problems
99
+
100
+ node_problems = []
101
+ if parsed_list.children:
102
+ node_problems = _create_node_tree(parsed_list.children, new_iri, create_client, project_iri)
103
+
104
+ return new_iri, node_problems
105
+
106
+
107
+ def _serialise_list(parsed_list_info: ParsedNodeInfo, project_iri: str) -> dict[str, Any]:
108
+ node_dict = {
109
+ "projectIri": project_iri,
110
+ "name": parsed_list_info.name,
111
+ "labels": _convert_to_api_format(parsed_list_info.labels),
112
+ }
113
+ if parsed_list_info.comments:
114
+ node_dict["comments"] = _convert_to_api_format(parsed_list_info.comments)
115
+ return node_dict
116
+
117
+
118
+ def _create_node_tree(
119
+ nodes: list[ParsedListNode], parent_iri: str, create_client: ListCreateClient, project_iri: str
120
+ ) -> list[UploadProblem]:
121
+ problems: list[UploadProblem] = []
122
+
123
+ for node in nodes:
124
+ serialised = _serialise_node(node.node_info, parent_iri, project_iri)
125
+ node_iri = create_client.add_list_node(serialised, parent_iri)
126
+ if node_iri is None:
127
+ problems.append(UploadProblem(node.node_info.name, ProblemType.LIST_NODE_COULD_NOT_BE_CREATED))
128
+ continue
129
+
130
+ if node.children:
131
+ child_problems = _create_node_tree(node.children, node_iri, create_client, project_iri)
132
+ problems.extend(child_problems)
133
+
134
+ return problems
135
+
136
+
137
+ def _serialise_node(node_info: ParsedNodeInfo, parent_iri: str, project_iri: str) -> dict[str, Any]:
138
+ node_dict = {
139
+ "parentNodeIri": parent_iri,
140
+ "projectIri": project_iri,
141
+ "name": node_info.name,
142
+ "labels": _convert_to_api_format(node_info.labels),
143
+ }
144
+ if node_info.comments:
145
+ node_dict["comments"] = _convert_to_api_format(node_info.comments)
146
+ return node_dict
147
+
148
+
149
+ def _convert_to_api_format(lang_dict: dict[str, str]) -> list[dict[str, str]]:
150
+ 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
@@ -11,12 +11,25 @@ from dsp_tools.commands.create.models.parsed_project import ParsedPermissions
11
11
  from dsp_tools.commands.create.models.parsed_project import ParsedProject
12
12
  from dsp_tools.commands.create.models.parsed_project import ParsedProjectMetadata
13
13
  from dsp_tools.commands.create.models.parsed_project import ParsedUser
14
+ from dsp_tools.commands.create.parsing.parse_lists import parse_list_section
14
15
  from dsp_tools.commands.create.parsing.parse_ontology import parse_ontology
15
16
  from dsp_tools.commands.create.parsing.parsing_utils import create_prefix_lookup
16
17
  from dsp_tools.commands.project.create.project_validate import validate_project
17
18
  from dsp_tools.utils.json_parsing import parse_json_input
18
19
 
19
20
 
21
+ def parse_lists_only(
22
+ project_file_as_path_or_parsed: str | Path | dict[str, Any],
23
+ ) -> tuple[ParsedProjectMetadata, list[ParsedList]] | CollectedProblems:
24
+ complete_json = _parse_and_validate(project_file_as_path_or_parsed)
25
+ project_json = complete_json["project"]
26
+ project_metadata = _parse_metadata(project_json)
27
+ parsed_lists, problems = _parse_lists(project_json)
28
+ if isinstance(problems, CollectedProblems):
29
+ return problems
30
+ return project_metadata, parsed_lists
31
+
32
+
20
33
  def parse_project(
21
34
  project_file_as_path_or_parsed: str | Path | dict[str, Any], api_url: str
22
35
  ) -> ParsedProject | list[CollectedProblems]:
@@ -28,6 +41,9 @@ def _parse_project(complete_json: dict[str, Any], api_url: str) -> ParsedProject
28
41
  prefix_lookup = create_prefix_lookup(complete_json, api_url)
29
42
  project_json = complete_json["project"]
30
43
  ontologies, failures = _parse_all_ontologies(project_json, prefix_lookup)
44
+ parsed_lists, problems = _parse_lists(project_json)
45
+ if isinstance(problems, CollectedProblems):
46
+ failures.append(problems)
31
47
  if failures:
32
48
  return failures
33
49
  return ParsedProject(
@@ -36,7 +52,7 @@ def _parse_project(complete_json: dict[str, Any], api_url: str) -> ParsedProject
36
52
  permissions=_parse_permissions(project_json),
37
53
  groups=_parse_groups(project_json),
38
54
  users=_parse_users(project_json),
39
- lists=_parse_lists(project_json),
55
+ lists=parsed_lists,
40
56
  ontologies=ontologies,
41
57
  )
42
58
 
@@ -79,10 +95,13 @@ def _parse_users(project_json: dict[str, Any]) -> list[ParsedUser]:
79
95
  return [ParsedUser(x) for x in found]
80
96
 
81
97
 
82
- def _parse_lists(project_json: dict[str, Any]) -> list[ParsedList]:
98
+ def _parse_lists(project_json: dict[str, Any]) -> tuple[list[ParsedList], CollectedProblems | None]:
83
99
  if not (found := project_json.get("lists")):
84
- return []
85
- return [ParsedList(x["name"], x) for x in found]
100
+ return [], None
101
+ result = parse_list_section(found)
102
+ if isinstance(result, CollectedProblems):
103
+ return [], result
104
+ return result, None
86
105
 
87
106
 
88
107
  def _parse_all_ontologies(
@@ -43,6 +43,7 @@ def ingest_xmlupload(
43
43
  skip_validation: bool = False,
44
44
  skip_ontology_validation: bool = False,
45
45
  id2iri_replacement_file: str | None = None,
46
+ do_not_request_resource_metadata_from_db: bool = False,
46
47
  ) -> bool:
47
48
  """
48
49
  This function reads an XML file
@@ -59,6 +60,8 @@ def ingest_xmlupload(
59
60
  skip_validation: skip the SHACL validation
60
61
  skip_ontology_validation: skip the ontology validation
61
62
  id2iri_replacement_file: to replace internal IDs of an XML file by IRIs provided in this mapping file
63
+ do_not_request_resource_metadata_from_db: if true do not request metadata information from the api
64
+ for existing resources
62
65
 
63
66
  Returns:
64
67
  True if all resources could be uploaded without errors; False if one of the resources could not be
@@ -115,6 +118,7 @@ def ingest_xmlupload(
115
118
  ignore_duplicate_files_warning=True,
116
119
  is_on_prod_server=is_on_prod_like_server,
117
120
  skip_ontology_validation=skip_ontology_validation,
121
+ do_not_request_resource_metadata_from_db=do_not_request_resource_metadata_from_db,
118
122
  ),
119
123
  auth=auth,
120
124
  )