fairgraph 0.13.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.
Files changed (267) hide show
  1. fairgraph/__init__.py +61 -0
  2. fairgraph/base.py +104 -0
  3. fairgraph/caching.py +52 -0
  4. fairgraph/client.py +867 -0
  5. fairgraph/collection.py +104 -0
  6. fairgraph/embedded.py +122 -0
  7. fairgraph/errors.py +47 -0
  8. fairgraph/fields.py +11 -0
  9. fairgraph/kgobject.py +1087 -0
  10. fairgraph/kgproxy.py +178 -0
  11. fairgraph/kgquery.py +151 -0
  12. fairgraph/node.py +488 -0
  13. fairgraph/openminds/__init__.py +1 -0
  14. fairgraph/openminds/chemicals/__init__.py +40 -0
  15. fairgraph/openminds/chemicals/amount_of_chemical.py +23 -0
  16. fairgraph/openminds/chemicals/chemical_mixture.py +72 -0
  17. fairgraph/openminds/chemicals/chemical_substance.py +67 -0
  18. fairgraph/openminds/chemicals/product_source.py +57 -0
  19. fairgraph/openminds/computation/__init__.py +53 -0
  20. fairgraph/openminds/computation/data_analysis.py +104 -0
  21. fairgraph/openminds/computation/data_copy.py +104 -0
  22. fairgraph/openminds/computation/environment.py +66 -0
  23. fairgraph/openminds/computation/generic_computation.py +104 -0
  24. fairgraph/openminds/computation/hardware_system.py +53 -0
  25. fairgraph/openminds/computation/launch_configuration.py +68 -0
  26. fairgraph/openminds/computation/local_file.py +83 -0
  27. fairgraph/openminds/computation/model_validation.py +107 -0
  28. fairgraph/openminds/computation/optimization.py +104 -0
  29. fairgraph/openminds/computation/simulation.py +104 -0
  30. fairgraph/openminds/computation/software_agent.py +81 -0
  31. fairgraph/openminds/computation/validation_test.py +105 -0
  32. fairgraph/openminds/computation/validation_test_version.py +180 -0
  33. fairgraph/openminds/computation/visualization.py +104 -0
  34. fairgraph/openminds/computation/workflow_execution.py +44 -0
  35. fairgraph/openminds/computation/workflow_recipe.py +95 -0
  36. fairgraph/openminds/computation/workflow_recipe_version.py +185 -0
  37. fairgraph/openminds/controlled_terms/__init__.py +116 -0
  38. fairgraph/openminds/controlled_terms/action_status_type.py +97 -0
  39. fairgraph/openminds/controlled_terms/age_category.py +89 -0
  40. fairgraph/openminds/controlled_terms/analysis_technique.py +108 -0
  41. fairgraph/openminds/controlled_terms/anatomical_axes_orientation.py +89 -0
  42. fairgraph/openminds/controlled_terms/anatomical_identification_type.py +89 -0
  43. fairgraph/openminds/controlled_terms/anatomical_plane.py +89 -0
  44. fairgraph/openminds/controlled_terms/annotation_criteria_type.py +89 -0
  45. fairgraph/openminds/controlled_terms/annotation_type.py +89 -0
  46. fairgraph/openminds/controlled_terms/atlas_type.py +88 -0
  47. fairgraph/openminds/controlled_terms/auditory_stimulus_type.py +127 -0
  48. fairgraph/openminds/controlled_terms/biological_order.py +117 -0
  49. fairgraph/openminds/controlled_terms/biological_process.py +79 -0
  50. fairgraph/openminds/controlled_terms/biological_sex.py +132 -0
  51. fairgraph/openminds/controlled_terms/breeding_type.py +127 -0
  52. fairgraph/openminds/controlled_terms/cell_culture_type.py +117 -0
  53. fairgraph/openminds/controlled_terms/cell_type.py +151 -0
  54. fairgraph/openminds/controlled_terms/chemical_mixture_type.py +89 -0
  55. fairgraph/openminds/controlled_terms/colormap.py +79 -0
  56. fairgraph/openminds/controlled_terms/contribution_type.py +79 -0
  57. fairgraph/openminds/controlled_terms/cranial_window_construction_type.py +89 -0
  58. fairgraph/openminds/controlled_terms/cranial_window_reinforcement_type.py +89 -0
  59. fairgraph/openminds/controlled_terms/criteria_quality_type.py +89 -0
  60. fairgraph/openminds/controlled_terms/data_type.py +89 -0
  61. fairgraph/openminds/controlled_terms/device_type.py +94 -0
  62. fairgraph/openminds/controlled_terms/difference_measure.py +89 -0
  63. fairgraph/openminds/controlled_terms/disease.py +142 -0
  64. fairgraph/openminds/controlled_terms/disease_model.py +142 -0
  65. fairgraph/openminds/controlled_terms/educational_level.py +79 -0
  66. fairgraph/openminds/controlled_terms/electrical_stimulus_type.py +137 -0
  67. fairgraph/openminds/controlled_terms/ethics_assessment.py +79 -0
  68. fairgraph/openminds/controlled_terms/experimental_approach.py +79 -0
  69. fairgraph/openminds/controlled_terms/file_bundle_grouping.py +99 -0
  70. fairgraph/openminds/controlled_terms/file_repository_type.py +89 -0
  71. fairgraph/openminds/controlled_terms/file_usage_role.py +89 -0
  72. fairgraph/openminds/controlled_terms/genetic_strain_type.py +127 -0
  73. fairgraph/openminds/controlled_terms/gustatory_stimulus_type.py +127 -0
  74. fairgraph/openminds/controlled_terms/handedness.py +127 -0
  75. fairgraph/openminds/controlled_terms/language.py +88 -0
  76. fairgraph/openminds/controlled_terms/laterality.py +94 -0
  77. fairgraph/openminds/controlled_terms/learning_resource_type.py +88 -0
  78. fairgraph/openminds/controlled_terms/measured_quantity.py +89 -0
  79. fairgraph/openminds/controlled_terms/measured_signal_type.py +79 -0
  80. fairgraph/openminds/controlled_terms/meta_data_model_type.py +88 -0
  81. fairgraph/openminds/controlled_terms/model_abstraction_level.py +89 -0
  82. fairgraph/openminds/controlled_terms/model_scope.py +89 -0
  83. fairgraph/openminds/controlled_terms/molecular_entity.py +142 -0
  84. fairgraph/openminds/controlled_terms/mri_pulse_sequence.py +98 -0
  85. fairgraph/openminds/controlled_terms/mri_weighting.py +98 -0
  86. fairgraph/openminds/controlled_terms/olfactory_stimulus_type.py +127 -0
  87. fairgraph/openminds/controlled_terms/operating_device.py +79 -0
  88. fairgraph/openminds/controlled_terms/operating_system.py +88 -0
  89. fairgraph/openminds/controlled_terms/optical_stimulus_type.py +127 -0
  90. fairgraph/openminds/controlled_terms/organ.py +161 -0
  91. fairgraph/openminds/controlled_terms/organism_substance.py +151 -0
  92. fairgraph/openminds/controlled_terms/organism_system.py +117 -0
  93. fairgraph/openminds/controlled_terms/patch_clamp_variation.py +89 -0
  94. fairgraph/openminds/controlled_terms/preparation_type.py +98 -0
  95. fairgraph/openminds/controlled_terms/product_accessibility.py +79 -0
  96. fairgraph/openminds/controlled_terms/programming_language.py +88 -0
  97. fairgraph/openminds/controlled_terms/qualitative_overlap.py +79 -0
  98. fairgraph/openminds/controlled_terms/semantic_data_type.py +79 -0
  99. fairgraph/openminds/controlled_terms/service.py +89 -0
  100. fairgraph/openminds/controlled_terms/setup_type.py +89 -0
  101. fairgraph/openminds/controlled_terms/software_application_category.py +79 -0
  102. fairgraph/openminds/controlled_terms/software_feature.py +79 -0
  103. fairgraph/openminds/controlled_terms/species.py +143 -0
  104. fairgraph/openminds/controlled_terms/stimulation_approach.py +98 -0
  105. fairgraph/openminds/controlled_terms/stimulation_technique.py +98 -0
  106. fairgraph/openminds/controlled_terms/subcellular_entity.py +143 -0
  107. fairgraph/openminds/controlled_terms/subject_attribute.py +89 -0
  108. fairgraph/openminds/controlled_terms/tactile_stimulus_type.py +127 -0
  109. fairgraph/openminds/controlled_terms/technique.py +108 -0
  110. fairgraph/openminds/controlled_terms/term_suggestion.py +121 -0
  111. fairgraph/openminds/controlled_terms/terminology.py +89 -0
  112. fairgraph/openminds/controlled_terms/tissue_sample_attribute.py +89 -0
  113. fairgraph/openminds/controlled_terms/tissue_sample_type.py +127 -0
  114. fairgraph/openminds/controlled_terms/type_of_uncertainty.py +89 -0
  115. fairgraph/openminds/controlled_terms/uberon_parcellation.py +153 -0
  116. fairgraph/openminds/controlled_terms/unit_of_measurement.py +108 -0
  117. fairgraph/openminds/controlled_terms/visual_stimulus_type.py +127 -0
  118. fairgraph/openminds/controlledterms.py +6 -0
  119. fairgraph/openminds/core/__init__.py +107 -0
  120. fairgraph/openminds/core/actors/__init__.py +7 -0
  121. fairgraph/openminds/core/actors/account_information.py +44 -0
  122. fairgraph/openminds/core/actors/affiliation.py +30 -0
  123. fairgraph/openminds/core/actors/consortium.py +175 -0
  124. fairgraph/openminds/core/actors/contact_information.py +43 -0
  125. fairgraph/openminds/core/actors/contribution.py +23 -0
  126. fairgraph/openminds/core/actors/organization.py +199 -0
  127. fairgraph/openminds/core/actors/person.py +236 -0
  128. fairgraph/openminds/core/data/__init__.py +13 -0
  129. fairgraph/openminds/core/data/content_type.py +107 -0
  130. fairgraph/openminds/core/data/content_type_pattern.py +53 -0
  131. fairgraph/openminds/core/data/copyright.py +23 -0
  132. fairgraph/openminds/core/data/file.py +275 -0
  133. fairgraph/openminds/core/data/file_archive.py +71 -0
  134. fairgraph/openminds/core/data/file_bundle.py +150 -0
  135. fairgraph/openminds/core/data/file_path_pattern.py +23 -0
  136. fairgraph/openminds/core/data/file_repository.py +99 -0
  137. fairgraph/openminds/core/data/file_repository_structure.py +51 -0
  138. fairgraph/openminds/core/data/hash.py +23 -0
  139. fairgraph/openminds/core/data/license.py +77 -0
  140. fairgraph/openminds/core/data/measurement.py +45 -0
  141. fairgraph/openminds/core/data/service_link.py +49 -0
  142. fairgraph/openminds/core/digital_identifier/__init__.py +11 -0
  143. fairgraph/openminds/core/digital_identifier/doi.py +98 -0
  144. fairgraph/openminds/core/digital_identifier/gridid.py +41 -0
  145. fairgraph/openminds/core/digital_identifier/handle.py +52 -0
  146. fairgraph/openminds/core/digital_identifier/identifiers_dot_org_id.py +41 -0
  147. fairgraph/openminds/core/digital_identifier/isbn.py +88 -0
  148. fairgraph/openminds/core/digital_identifier/issn.py +63 -0
  149. fairgraph/openminds/core/digital_identifier/orcid.py +41 -0
  150. fairgraph/openminds/core/digital_identifier/rorid.py +41 -0
  151. fairgraph/openminds/core/digital_identifier/rrid.py +55 -0
  152. fairgraph/openminds/core/digital_identifier/stock_number.py +23 -0
  153. fairgraph/openminds/core/digital_identifier/swhid.py +48 -0
  154. fairgraph/openminds/core/miscellaneous/__init__.py +7 -0
  155. fairgraph/openminds/core/miscellaneous/comment.py +47 -0
  156. fairgraph/openminds/core/miscellaneous/funding.py +70 -0
  157. fairgraph/openminds/core/miscellaneous/quantitative_value.py +43 -0
  158. fairgraph/openminds/core/miscellaneous/quantitative_value_array.py +49 -0
  159. fairgraph/openminds/core/miscellaneous/quantitative_value_range.py +43 -0
  160. fairgraph/openminds/core/miscellaneous/research_product_group.py +26 -0
  161. fairgraph/openminds/core/miscellaneous/web_resource.py +104 -0
  162. fairgraph/openminds/core/products/__init__.py +12 -0
  163. fairgraph/openminds/core/products/dataset.py +95 -0
  164. fairgraph/openminds/core/products/dataset_version.py +240 -0
  165. fairgraph/openminds/core/products/meta_data_model.py +95 -0
  166. fairgraph/openminds/core/products/meta_data_model_version.py +168 -0
  167. fairgraph/openminds/core/products/model.py +103 -0
  168. fairgraph/openminds/core/products/model_version.py +235 -0
  169. fairgraph/openminds/core/products/project.py +56 -0
  170. fairgraph/openminds/core/products/setup.py +69 -0
  171. fairgraph/openminds/core/products/software.py +95 -0
  172. fairgraph/openminds/core/products/software_version.py +226 -0
  173. fairgraph/openminds/core/products/web_service.py +103 -0
  174. fairgraph/openminds/core/products/web_service_version.py +182 -0
  175. fairgraph/openminds/core/research/__init__.py +17 -0
  176. fairgraph/openminds/core/research/behavioral_protocol.py +69 -0
  177. fairgraph/openminds/core/research/configuration.py +67 -0
  178. fairgraph/openminds/core/research/custom_property_set.py +27 -0
  179. fairgraph/openminds/core/research/numerical_property.py +23 -0
  180. fairgraph/openminds/core/research/property_value_list.py +71 -0
  181. fairgraph/openminds/core/research/protocol.py +67 -0
  182. fairgraph/openminds/core/research/protocol_execution.py +76 -0
  183. fairgraph/openminds/core/research/strain.py +90 -0
  184. fairgraph/openminds/core/research/string_property.py +23 -0
  185. fairgraph/openminds/core/research/subject.py +79 -0
  186. fairgraph/openminds/core/research/subject_group.py +91 -0
  187. fairgraph/openminds/core/research/subject_group_state.py +113 -0
  188. fairgraph/openminds/core/research/subject_state.py +138 -0
  189. fairgraph/openminds/core/research/tissue_sample.py +87 -0
  190. fairgraph/openminds/core/research/tissue_sample_collection.py +99 -0
  191. fairgraph/openminds/core/research/tissue_sample_collection_state.py +109 -0
  192. fairgraph/openminds/core/research/tissue_sample_state.py +127 -0
  193. fairgraph/openminds/ephys/__init__.py +39 -0
  194. fairgraph/openminds/ephys/activity/__init__.py +3 -0
  195. fairgraph/openminds/ephys/activity/cell_patching.py +73 -0
  196. fairgraph/openminds/ephys/activity/electrode_placement.py +67 -0
  197. fairgraph/openminds/ephys/activity/recording_activity.py +67 -0
  198. fairgraph/openminds/ephys/device/__init__.py +6 -0
  199. fairgraph/openminds/ephys/device/electrode.py +81 -0
  200. fairgraph/openminds/ephys/device/electrode_array.py +85 -0
  201. fairgraph/openminds/ephys/device/electrode_array_usage.py +105 -0
  202. fairgraph/openminds/ephys/device/electrode_usage.py +101 -0
  203. fairgraph/openminds/ephys/device/pipette.py +81 -0
  204. fairgraph/openminds/ephys/device/pipette_usage.py +123 -0
  205. fairgraph/openminds/ephys/entity/__init__.py +2 -0
  206. fairgraph/openminds/ephys/entity/channel.py +23 -0
  207. fairgraph/openminds/ephys/entity/recording.py +63 -0
  208. fairgraph/openminds/publications/__init__.py +47 -0
  209. fairgraph/openminds/publications/book.py +106 -0
  210. fairgraph/openminds/publications/chapter.py +100 -0
  211. fairgraph/openminds/publications/learning_resource.py +90 -0
  212. fairgraph/openminds/publications/live_paper.py +95 -0
  213. fairgraph/openminds/publications/live_paper_resource_item.py +58 -0
  214. fairgraph/openminds/publications/live_paper_section.py +57 -0
  215. fairgraph/openminds/publications/live_paper_version.py +177 -0
  216. fairgraph/openminds/publications/periodical.py +53 -0
  217. fairgraph/openminds/publications/publication_issue.py +44 -0
  218. fairgraph/openminds/publications/publication_volume.py +44 -0
  219. fairgraph/openminds/publications/scholarly_article.py +146 -0
  220. fairgraph/openminds/sands/__init__.py +57 -0
  221. fairgraph/openminds/sands/atlas/__init__.py +9 -0
  222. fairgraph/openminds/sands/atlas/atlas_annotation.py +52 -0
  223. fairgraph/openminds/sands/atlas/brain_atlas.py +113 -0
  224. fairgraph/openminds/sands/atlas/brain_atlas_version.py +213 -0
  225. fairgraph/openminds/sands/atlas/common_coordinate_space.py +121 -0
  226. fairgraph/openminds/sands/atlas/common_coordinate_space_version.py +243 -0
  227. fairgraph/openminds/sands/atlas/parcellation_entity.py +133 -0
  228. fairgraph/openminds/sands/atlas/parcellation_entity_version.py +162 -0
  229. fairgraph/openminds/sands/atlas/parcellation_terminology.py +38 -0
  230. fairgraph/openminds/sands/atlas/parcellation_terminology_version.py +38 -0
  231. fairgraph/openminds/sands/mathematical_shapes/__init__.py +3 -0
  232. fairgraph/openminds/sands/mathematical_shapes/circle.py +23 -0
  233. fairgraph/openminds/sands/mathematical_shapes/ellipse.py +27 -0
  234. fairgraph/openminds/sands/mathematical_shapes/rectangle.py +23 -0
  235. fairgraph/openminds/sands/miscellaneous/__init__.py +6 -0
  236. fairgraph/openminds/sands/miscellaneous/anatomical_target_position.py +40 -0
  237. fairgraph/openminds/sands/miscellaneous/coordinate_point.py +23 -0
  238. fairgraph/openminds/sands/miscellaneous/qualitative_relation_assessment.py +34 -0
  239. fairgraph/openminds/sands/miscellaneous/quantitative_relation_assessment.py +38 -0
  240. fairgraph/openminds/sands/miscellaneous/single_color.py +24 -0
  241. fairgraph/openminds/sands/miscellaneous/viewer_specification.py +40 -0
  242. fairgraph/openminds/sands/non_atlas/__init__.py +3 -0
  243. fairgraph/openminds/sands/non_atlas/custom_anatomical_entity.py +110 -0
  244. fairgraph/openminds/sands/non_atlas/custom_annotation.py +54 -0
  245. fairgraph/openminds/sands/non_atlas/custom_coordinate_space.py +67 -0
  246. fairgraph/openminds/specimen_prep/__init__.py +38 -0
  247. fairgraph/openminds/specimen_prep/activity/__init__.py +3 -0
  248. fairgraph/openminds/specimen_prep/activity/cranial_window_preparation.py +69 -0
  249. fairgraph/openminds/specimen_prep/activity/tissue_culture_preparation.py +67 -0
  250. fairgraph/openminds/specimen_prep/activity/tissue_sample_slicing.py +69 -0
  251. fairgraph/openminds/specimen_prep/device/__init__.py +2 -0
  252. fairgraph/openminds/specimen_prep/device/slicing_device.py +73 -0
  253. fairgraph/openminds/specimen_prep/device/slicing_device_usage.py +117 -0
  254. fairgraph/openminds/specimenprep.py +4 -0
  255. fairgraph/openminds/stimulation/__init__.py +38 -0
  256. fairgraph/openminds/stimulation/activity/__init__.py +1 -0
  257. fairgraph/openminds/stimulation/activity/stimulation_activity.py +67 -0
  258. fairgraph/openminds/stimulation/stimulus/__init__.py +1 -0
  259. fairgraph/openminds/stimulation/stimulus/ephys_stimulus.py +63 -0
  260. fairgraph/queries.py +499 -0
  261. fairgraph/registry.py +123 -0
  262. fairgraph/utility.py +607 -0
  263. fairgraph-0.13.0.dist-info/METADATA +222 -0
  264. fairgraph-0.13.0.dist-info/RECORD +267 -0
  265. fairgraph-0.13.0.dist-info/WHEEL +5 -0
  266. fairgraph-0.13.0.dist-info/licenses/LICENSE.txt +177 -0
  267. fairgraph-0.13.0.dist-info/top_level.txt +1 -0
fairgraph/client.py ADDED
@@ -0,0 +1,867 @@
1
+ """
2
+ This module defines the KGClient class, which provides a thin interface
3
+ on top of the kg-core-python package, for communicating with the
4
+ EBRAINS KG core API.
5
+ """
6
+
7
+ # Copyright 2018-2024 CNRS
8
+
9
+ # Licensed under the Apache License, Version 2.0 (the "License");
10
+ # you may not use this file except in compliance with the License.
11
+ # You may obtain a copy of the License at
12
+
13
+ # http://www.apache.org/licenses/LICENSE-2.0
14
+
15
+ # Unless required by applicable law or agreed to in writing, software
16
+ # distributed under the License is distributed on an "AS IS" BASIS,
17
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
18
+ # See the License for the specific language governing permissions and
19
+ # limitations under the License.
20
+
21
+ from __future__ import annotations
22
+ from copy import deepcopy
23
+ import os
24
+ import logging
25
+ from typing import Any, Dict, Iterable, List, Optional, Union, TYPE_CHECKING
26
+ from uuid import uuid4, UUID
27
+
28
+ try:
29
+ from kg_core.kg import kg
30
+ from kg_core.request import Stage, Pagination, ExtendedResponseConfiguration, ReleaseTreeScope
31
+ from kg_core.response import ResultPage, JsonLdDocument, SpaceInformation, Error
32
+
33
+ have_kg_core = True
34
+ except ImportError:
35
+ have_kg_core = False
36
+
37
+ from openminds.registry import lookup_type
38
+
39
+ from .errors import AuthenticationError, AuthorizationError, ResourceExistsError
40
+ from .utility import (
41
+ adapt_namespaces_for_query,
42
+ adapt_namespaces_3to4,
43
+ adapt_namespaces_4to3,
44
+ adapt_type_4to3,
45
+ handle_scope_keyword,
46
+ )
47
+ from .base import OPENMINDS_VERSION
48
+
49
+ if TYPE_CHECKING:
50
+ from .kgobject import KGObject
51
+
52
+ try:
53
+ import clb_nb_utils.oauth as clb_oauth # type: ignore
54
+ except ImportError:
55
+ clb_oauth = None
56
+
57
+
58
+ logger = logging.getLogger("fairgraph")
59
+
60
+
61
+ if have_kg_core:
62
+ STAGE_MAP = {
63
+ "released": Stage.RELEASED,
64
+ "latest": Stage.IN_PROGRESS,
65
+ "in progress": Stage.IN_PROGRESS,
66
+ }
67
+ default_response_configuration = ExtendedResponseConfiguration(return_embedded=True)
68
+
69
+
70
+ AVAILABLE_PERMISSIONS = [
71
+ "CREATE",
72
+ "READ",
73
+ "DELETE",
74
+ "RELEASE",
75
+ "INVITE_FOR_REVIEW",
76
+ "INVITE_FOR_SUGGESTION",
77
+ "SUGGEST",
78
+ "UNRELEASE",
79
+ "READ_RELEASED",
80
+ "RELEASE_STATUS",
81
+ "WRITE",
82
+ ]
83
+
84
+
85
+ class KGClient(object):
86
+ """
87
+ A client for accessing the EBRAINS Knowledge Graph (KG) API.
88
+
89
+ It can be used to retrieve, add, update, and delete KG nodes.
90
+
91
+ Attributes:
92
+ cache (dict): A dictionary used for caching JSON-LD documents.
93
+ accepted_terms_of_use (bool): A boolean indicating whether the user has accepted the terms of use.
94
+
95
+ Args:
96
+ token (str, optional): An EBRAINS authentication token for accessing the KG API.
97
+ host (str, optional): The hostname of the KG API. Use "core.kg-ppd.ebrains.eu" for testing
98
+ and "core.kg.ebrains.eu" to work with the production KG.
99
+ client_id (str, optional): For use together with client_secret in place of the token if you have a service account.
100
+ client_secret (str, optional): The client secret to use for authentication. Required if client_id is provided.
101
+ allow_interactive (bool, default True): if true, allow authentication via web browser
102
+
103
+ Raises:
104
+ ImportError: If the kg_core package is not installed.
105
+ AuthenticationError: If neither a token nor client ID/secret are provided.
106
+ """
107
+
108
+ def __init__(
109
+ self,
110
+ token: Optional[str] = None,
111
+ host: str = "core.kg-ppd.ebrains.eu",
112
+ client_id: Optional[str] = None,
113
+ client_secret: Optional[str] = None,
114
+ allow_interactive: bool = True,
115
+ ):
116
+ if not have_kg_core:
117
+ raise ImportError("Please install the ebrains-kg-core package")
118
+ if client_id and client_secret:
119
+ self._kg_client_builder = kg(host).with_credentials(client_id, client_secret)
120
+ elif token:
121
+ self._kg_client_builder = kg(host).with_token(token)
122
+ elif clb_oauth:
123
+ self._kg_client_builder = kg(host).with_token(clb_oauth.get_token()) # running in EBRAINS Jupyter Lab
124
+ else:
125
+ try:
126
+ self._kg_client_builder = kg(host).with_token(os.environ["KG_AUTH_TOKEN"])
127
+ except KeyError:
128
+ if allow_interactive:
129
+ iam_config_url = "https://iam.ebrains.eu/auth/realms/hbp/.well-known/openid-configuration"
130
+ self._kg_client_builder = kg(host).with_device_flow(
131
+ client_id="kg-core-python", open_id_configuration_url=iam_config_url
132
+ )
133
+ else:
134
+ raise AuthenticationError("Need to provide either token or client id/secret.")
135
+ self._kg_client = self._kg_client_builder.build()
136
+ self.__kg_admin_client = None
137
+ self.host = host
138
+ self._user_info = None
139
+ self.cache: Dict[str, JsonLdDocument] = {}
140
+ self._query_cache: Dict[str, str] = {}
141
+ self.accepted_terms_of_use = False
142
+ self._migrated = None
143
+ if allow_interactive:
144
+ self.user_info()
145
+
146
+ @property
147
+ def _kg_admin_client(self):
148
+ if self.__kg_admin_client is None:
149
+ self.__kg_admin_client = self._kg_client_builder.build_admin()
150
+ return self.__kg_admin_client
151
+
152
+ @property
153
+ def token(self) -> Optional[str]:
154
+ return self._kg_client.instances._kg_config.token_handler._fetch_token()
155
+
156
+ def _check_response(
157
+ self,
158
+ response: ResultPage[JsonLdDocument],
159
+ ignore_not_found: bool = False,
160
+ error_context: str = "",
161
+ expected_instance_id: Optional[str] = None,
162
+ ) -> ResultPage[JsonLdDocument]:
163
+ if expected_instance_id and not response.error:
164
+ if response.total > 1:
165
+ # if an instance_id is specified, we expect there to be only one result
166
+ # if there are more, it seems to mean that the instance_id does not exist
167
+ response.error = Error(
168
+ code=404,
169
+ message=(
170
+ f"Received multiple results when specifying instance_id {expected_instance_id}"
171
+ "This indicates the instance does not exist."
172
+ ),
173
+ )
174
+ response.data = []
175
+ response.size = response.total = 0
176
+ else:
177
+ if response.size == 1:
178
+ if str(expected_instance_id) not in response.data[0]["@id"]:
179
+ raise Exception("mismatched instance_id")
180
+ if response.error:
181
+ # todo: handle "ignore_not_found"
182
+ if response.error.code == 403:
183
+ raise AuthorizationError(f"{response} {error_context}")
184
+ elif response.error.code == 401:
185
+ raise AuthenticationError(f"{response} {error_context}")
186
+ elif response.error.code == 404 and ignore_not_found:
187
+ return response
188
+ elif response.error.code == 409:
189
+ raise ResourceExistsError(f"{response} {error_context}")
190
+ else:
191
+ raise Exception(f"Error: {response.error} {error_context}")
192
+ else:
193
+ if self.migrated is False:
194
+ adapt_namespaces_3to4(response.data)
195
+ return response
196
+
197
+ @property
198
+ def migrated(self):
199
+ # This is a temporary work-around for use during the transitional period
200
+ # from openMINDS v3 to v4 (change of namespace)
201
+ if self._migrated is None:
202
+ self._migrated = True # to stop the call to _check_response() in instance_from_full_uri from recurring
203
+
204
+ # This is the released controlled term for "left handedness", which should be accessible to everyone
205
+ result = self.instance_from_full_uri(
206
+ "https://kg.ebrains.eu/api/instances/92631f2e-fc6e-4122-8015-a0731c67f66c", release_status="released"
207
+ )
208
+ if "om-i.org" in result["@type"]:
209
+ self._migrated = True
210
+ else:
211
+ self._migrated = False
212
+ return self._migrated
213
+
214
+ def query(
215
+ self,
216
+ query: Dict[str, Any],
217
+ filter: Optional[Dict[str, str]] = None,
218
+ instance_id: Optional[str] = None,
219
+ from_index: int = 0,
220
+ size: int = 100,
221
+ release_status: str = "released",
222
+ scope: Optional[str] = None,
223
+ id_key: str = "@id",
224
+ use_stored_query: bool = False,
225
+ restrict_to_spaces: Optional[List[str]] = None,
226
+ ) -> ResultPage[JsonLdDocument]:
227
+ """
228
+ Execute a Knowledge Graph (KG) query with the given filters and query definition.
229
+
230
+ Args:
231
+ query (Dict[str, Any]): A dictionary containing the query definition in JSON-LD.
232
+ filter (Dict[str, str]): A dictionary of filters to apply to the query. Each key represents the prop name to filter on,
233
+ and the value represents the value(s) to filter on.
234
+ instance_id (Optional[URI]): The URI of a specific KG instance to retrieve.
235
+ from_index (int): The index of the first result to return (0-based).
236
+ size (int): The maximum number of results to return.
237
+ release_status (str): The scope of the query. Valid values are "released", "in progress", or "any". Default is "released".
238
+ id_key (str): The key that identifies the ID of a JSON-LD document. Default is "@id".
239
+ use_stored_query (bool): Whether to use a stored query with the given query_id instead of a dynamic query. Default is False.
240
+
241
+ Returns:
242
+ A ResultPage object containing a list of JSON-LD instances that satisfy the query,
243
+ along with metadata about the query results such as total number of instances, and pagination information.
244
+ """
245
+ release_status = handle_scope_keyword(scope, release_status)
246
+ query_id = query.get("@id", None)
247
+
248
+ if use_stored_query:
249
+
250
+ def _query(release_status, from_index, size):
251
+ response = self._kg_client.queries.execute_query_by_id(
252
+ query_id=self.uuid_from_uri(query_id),
253
+ additional_request_params=filter or {},
254
+ stage=STAGE_MAP[release_status],
255
+ pagination=Pagination(start=from_index, size=size),
256
+ instance_id=instance_id,
257
+ restrict_to_spaces=restrict_to_spaces,
258
+ )
259
+ error_context = f"_query(release_status={release_status} query_id={query_id} filter={filter} instance_id={instance_id} size={size} from_index={from_index})"
260
+ return self._check_response(
261
+ response, error_context=error_context, expected_instance_id=instance_id, ignore_not_found=True
262
+ )
263
+
264
+ else:
265
+ if self.migrated is False:
266
+ query = adapt_namespaces_for_query(query)
267
+
268
+ def _query(release_status, from_index, size):
269
+ response = self._kg_client.queries.test_query(
270
+ query,
271
+ additional_request_params=filter or {},
272
+ stage=STAGE_MAP[release_status],
273
+ pagination=Pagination(start=from_index, size=size),
274
+ instance_id=instance_id,
275
+ restrict_to_spaces=restrict_to_spaces,
276
+ )
277
+ error_context = f"_query(release_status={release_status} query_id={query_id} filter={filter} instance_id={instance_id} size={size} from_index={from_index})"
278
+ return self._check_response(
279
+ response, error_context=error_context, expected_instance_id=instance_id, ignore_not_found=True
280
+ )
281
+
282
+ if release_status == "any":
283
+ # the following implementation is simple but very inefficient
284
+ # because we retrieve _all_ instances and then apply the limits
285
+ # from_index and size.
286
+ # todo: make this more efficient, but be sure to clearly
287
+ # explain the algorithm
288
+ instances = {}
289
+ # first we get the released instances
290
+ response = _query("released", 0, 100000)
291
+ for instance in response.data:
292
+ instances[instance[id_key]] = instance
293
+ # now we get the "in progress" instances, and overwrite
294
+ # any existing released instances which have the same id
295
+ response = _query("in progress", 0, 100000)
296
+ for instance in response.data:
297
+ instances[instance[id_key]] = instance
298
+ response.data = list(instances.values())[from_index : from_index + size]
299
+ response.size = len(response.data)
300
+ response.total = len(instances)
301
+ else:
302
+ response = _query(release_status, from_index, size)
303
+ return response
304
+
305
+ def list(
306
+ self,
307
+ target_type: str,
308
+ space: Optional[str] = None,
309
+ from_index: int = 0,
310
+ size: int = 100,
311
+ release_status: str = "released",
312
+ scope: Optional[str] = None,
313
+ ) -> ResultPage[JsonLdDocument]:
314
+ """
315
+ List KG instances of a given type.
316
+
317
+ Args:
318
+ target_type: The URI if the instance type to list.
319
+ space: If specified, restricts the search to the given space.
320
+ from_index: The index of the first result to include in the response.
321
+ size: The maximum number of results to include in the response.
322
+ release_status: The scope of instances to include in the response. Valid values are
323
+ 'released', 'in progress', 'any'. If 'any' is specified, all accessible instances
324
+ are included in the response, but this may be slow where there are large numbers of instances.
325
+
326
+ Returns:
327
+ A ResultPage object containing the list of JSON-LD instances,
328
+ along with metadata about the query results such as total number of instances, and pagination information.
329
+ """
330
+ release_status = handle_scope_keyword(scope, release_status)
331
+
332
+ if self.migrated is False:
333
+ target_type = adapt_type_4to3(target_type)
334
+
335
+ def _list(release_status, from_index, size):
336
+ response = self._kg_client.instances.list(
337
+ stage=STAGE_MAP[release_status],
338
+ target_type=target_type,
339
+ space=space,
340
+ response_configuration=default_response_configuration,
341
+ pagination=Pagination(start=from_index, size=size),
342
+ )
343
+ error_context = f"_list(release_status={release_status} space={space} target_type={target_type} size={size} from_index={from_index})"
344
+ return self._check_response(response, error_context=error_context)
345
+
346
+ if release_status == "any":
347
+ # see comments in query() about this implementation
348
+ instances = {}
349
+ # first we get the released instances
350
+ response = _list("released", 0, 100000)
351
+ for instance in response.data:
352
+ instances[instance["@id"]] = instance
353
+ # now we get the "in progress" instances, and overwrite
354
+ # any existing released instances which have the same id
355
+ response = _list("in progress", 0, 100000)
356
+ for instance in response.data:
357
+ instances[instance["@id"]] = instance
358
+ response.data = list(instances.values())[from_index : from_index + size]
359
+ response.size = len(response.data)
360
+ response.total = len(instances)
361
+ return response
362
+ else:
363
+ return _list(release_status, from_index, size)
364
+
365
+ def instance_from_full_uri(
366
+ self,
367
+ uri: str,
368
+ use_cache: bool = True,
369
+ release_status: str = "released",
370
+ scope: Optional[str] = None,
371
+ require_full_data: bool = True,
372
+ ) -> JsonLdDocument:
373
+ """
374
+ Return a specific KG instance identified by its URI.
375
+
376
+ Args:
377
+ uri: The global identifier of the instance
378
+ use_cache: whether to use cached data if they exist. Defaults to True.
379
+ release_status: The release_status of instances to include in the response.
380
+ Valid values are 'released', 'in progress', 'any'.
381
+ require_full_data: Whether to only return instances for which the user has full read access.
382
+ """
383
+ release_status = handle_scope_keyword(scope, release_status)
384
+ logger.debug("Retrieving instance from {}, api='core' use_cache={}".format(uri, use_cache))
385
+ data: JsonLdDocument
386
+ if use_cache and uri in self.cache:
387
+ logger.debug("Retrieving instance {} from cache".format(uri))
388
+ data = self.cache[uri]
389
+ else:
390
+
391
+ def _get_instance(release_status):
392
+ error_context = f"_get_instance(release_status={release_status} uri={uri})"
393
+ # Normal KG URIs start with https://kg.ebrains.eu/api/instances/ with a UUID
394
+ # but for openMINDS controlled terms we may have the openMINDS URI
395
+ # of the form https://openminds.ebrains.eu/instances/ageCategory/juvenile (v3)
396
+ # or https://openminds.om-i.org/instances/ageCategory/juvenile (v4)
397
+ # We use different query methods for these different cases.
398
+ kg_namespace = self._kg_client.instances._kg_config.id_namespace
399
+ if uri.startswith(kg_namespace):
400
+ response = self._kg_client.instances.get_by_id(
401
+ stage=STAGE_MAP[release_status],
402
+ instance_id=self.uuid_from_uri(uri),
403
+ extended_response_configuration=default_response_configuration,
404
+ )
405
+ response = self._check_response(response, error_context=error_context, ignore_not_found=True)
406
+ if response.error:
407
+ assert response.error.code == 404 # all other errors should have been trapped by the check
408
+ data = None
409
+ else:
410
+ data = response.data
411
+ elif uri.startswith("https://openminds.om-i.org/instances") or uri.startswith(
412
+ "https://openminds.ebrains.eu/instances"
413
+ ):
414
+ if self.migrated and uri.startswith("https://openminds.ebrains.eu"):
415
+ payload = [uri.replace("ebrains.eu", "om-i.org")]
416
+ elif uri.startswith("https://openminds.om-i.org"):
417
+ payload = [uri.replace("om-i.org", "ebrains.eu")]
418
+ else:
419
+ payload = [uri]
420
+ response = self._kg_client.instances.get_by_identifiers(
421
+ stage=STAGE_MAP[release_status],
422
+ payload=payload,
423
+ extended_response_configuration=default_response_configuration,
424
+ )
425
+ # todo: handle errors
426
+ data = response.data[payload[0]].data
427
+ else:
428
+ raise Exception(f"This client cannot retrieve instances from {uri}")
429
+
430
+ # in some circumstances, the KG returns "minimal" metadata,
431
+ # e.g. with just the id and fullName properties
432
+ # this means the user does not have full access, so we count this as no data
433
+ if require_full_data and data and "http://schema.org/identifier" not in data:
434
+ data = None
435
+ return data
436
+
437
+ if release_status == "any":
438
+ data_ip = _get_instance("in progress")
439
+ data_rel = _get_instance("released")
440
+ data = data_rel or data_ip
441
+ if data_ip is not None:
442
+ data.update(data_ip)
443
+ else:
444
+ data = _get_instance(release_status)
445
+
446
+ if data:
447
+ self.cache[uri] = data
448
+ return data
449
+
450
+ def create_new_instance(
451
+ self, data: JsonLdDocument, space: str, instance_id: Optional[str] = None
452
+ ) -> JsonLdDocument:
453
+ """
454
+ Create a new KG instance using the data provided.
455
+
456
+ Args:
457
+ data (dict): a JSON-LD document that should be added to the KG as a new instance.
458
+ space (str): the space in which the instance should be stored.
459
+ instance_id (UUID, optional): a UUID that should be used as the basis for the
460
+ instance's persistent identifier. If not specified, the KG will generate an ID.
461
+ """
462
+ if "'@id': None" in str(data):
463
+ raise ValueError("payload contains undefined ids")
464
+ if instance_id:
465
+ UUID(instance_id)
466
+ if self.migrated is False:
467
+ data = deepcopy(data)
468
+ adapt_namespaces_4to3(data)
469
+ if instance_id:
470
+ response = self._kg_client.instances.create_new_with_id(
471
+ space=space,
472
+ payload=data,
473
+ instance_id=instance_id,
474
+ extended_response_configuration=default_response_configuration,
475
+ )
476
+ else:
477
+ response = self._kg_client.instances.create_new(
478
+ space=space,
479
+ payload=data,
480
+ extended_response_configuration=default_response_configuration,
481
+ )
482
+ error_context = f"create_new_instance(data={data}, space={space}, instance_id={instance_id})"
483
+ return self._check_response(response, error_context=error_context).data
484
+
485
+ def update_instance(self, instance_id: str, data: JsonLdDocument) -> JsonLdDocument:
486
+ """
487
+ Update an existing KG instance using the data provided.
488
+
489
+ Args:
490
+ instance_id (UUID): the instance's persistent identifier.
491
+ data (dict): a JSON-LD document that modifies some or all of the data of the existing instance.
492
+ """
493
+ UUID(instance_id)
494
+ if self.migrated is False:
495
+ data = deepcopy(data)
496
+ adapt_namespaces_4to3(data)
497
+ response = self._kg_client.instances.contribute_to_partial_replacement(
498
+ instance_id=instance_id,
499
+ payload=data,
500
+ extended_response_configuration=default_response_configuration,
501
+ )
502
+ error_context = f"update_instance(data={data}, instance_id={instance_id})"
503
+ return self._check_response(response, error_context=error_context).data
504
+
505
+ def replace_instance(self, instance_id: str, data: JsonLdDocument) -> JsonLdDocument:
506
+ """
507
+ Replace an existing KG instance using the data provided.
508
+
509
+ Args:
510
+ instance_id (UUID): the instance's persistent identifier.
511
+ data (dict): a JSON-LD document that will replace the existing instance.
512
+ """
513
+ UUID(instance_id)
514
+ if self.migrated is False:
515
+ data = deepcopy(data)
516
+ adapt_namespaces_4to3(data)
517
+ response = self._kg_client.instances.contribute_to_full_replacement(
518
+ instance_id=instance_id,
519
+ payload=data,
520
+ extended_response_configuration=default_response_configuration,
521
+ )
522
+ error_context = f"replace_instance(data={data}, instance_id={instance_id})"
523
+ return self._check_response(response, error_context=error_context).data
524
+
525
+ def delete_instance(self, instance_id: str, ignore_not_found: bool = True, ignore_errors: bool = True):
526
+ """
527
+ Delete a KG instance.
528
+ """
529
+ UUID(instance_id)
530
+ response = self._kg_client.instances.delete(instance_id)
531
+ # response is None if no errors
532
+ if response: # error
533
+ if not ignore_errors:
534
+ raise Exception(response.message)
535
+ return response
536
+
537
+ def uri_from_uuid(self, uuid: str) -> str:
538
+ """Return an instance's URI given its UUID."""
539
+ namespace = self._kg_client.instances._kg_config.id_namespace
540
+ return f"{namespace}{uuid}"
541
+
542
+ def uuid_from_uri(self, uri: str) -> UUID:
543
+ """Return an instance's UUID given its URI."""
544
+ namespace = self._kg_client.instances._kg_config.id_namespace
545
+ assert uri.startswith(namespace)
546
+ return UUID(uri[len(namespace) :])
547
+
548
+ def store_query(self, query_label: str, query_definition: Dict[str, Any], space: str):
549
+ """
550
+ Store a query definition in the KG.
551
+
552
+ Args:
553
+ query_label (str): a label that can be used to identify and retrieve the query definition.
554
+ query_definition (dict): a JSON-LD document defining a KG query.
555
+ space (str): the space in which the query definition should be stored.
556
+ """
557
+ existing_query = self.retrieve_query(query_label)
558
+ if existing_query:
559
+ query_id = self.uuid_from_uri(existing_query["@id"])
560
+ else:
561
+ query_id = uuid4()
562
+
563
+ try:
564
+ response = self._check_response(
565
+ self._kg_client.queries.save_query(
566
+ query_id=query_id, payload=query_definition, space=space or "myspace"
567
+ )
568
+ )
569
+ except AuthorizationError:
570
+ response = self._check_response(
571
+ self._kg_client.queries.save_query(query_id=query_id, payload=query_definition, space="myspace")
572
+ )
573
+
574
+ query_definition["@id"] = self.uri_from_uuid(query_id)
575
+
576
+ def retrieve_query(self, query_label: str) -> Dict[str, Any]:
577
+ """
578
+ Retrieve a stored query definition from the KG.
579
+
580
+ Args:
581
+ query_label (str): the label of the query definition to be retrieved.
582
+ """
583
+ if query_label not in self._query_cache:
584
+ response = self._check_response(self._kg_client.queries.list_per_root_type(search=query_label))
585
+ if response.total == 0:
586
+ return None
587
+ elif response.total > 1:
588
+ # check for exact match to query_label
589
+ found_match = False
590
+ kgvq = "https://core.kg.ebrains.eu/vocab/query"
591
+ for result in response.data:
592
+ if result[f"{kgvq}/meta"][f"{kgvq}/name"] == query_label:
593
+ query_definition = result
594
+ found_match = True
595
+ break
596
+ if not found_match:
597
+ return None
598
+ else:
599
+ query_definition = response.data[0]
600
+ self._query_cache[query_label] = query_definition
601
+ return self._query_cache[query_label]
602
+
603
+ def user_info(self) -> Dict[str, Any]:
604
+ """
605
+ Returns information about the current user.
606
+
607
+ This information is that associated with the authorization token used.
608
+ """
609
+ if self._user_info is None:
610
+ response = self._kg_client.users.my_info()
611
+ if response.data:
612
+ self._user_info = response.data
613
+ elif response.error.code == 401:
614
+ raise AuthenticationError()
615
+ elif response.error.code == 403:
616
+ raise AuthorizationError()
617
+ else:
618
+ raise Exception(response.error)
619
+ return self._user_info
620
+
621
+ def spaces(
622
+ self, permissions: Optional[Iterable[str] | bool] = None, names_only: bool = False
623
+ ) -> Union[List[str], List[SpaceInformation]]:
624
+ f"""
625
+ Return a list of the Knowledge Graph spaces the user can access.
626
+
627
+ Args:
628
+ permissions (Optional[Iterable[str]]): Return only spaces for which the user has specific permissions.
629
+ The available permissions are as follows: {AVAILABLE_PERMISSIONS}
630
+ names_only (bool): Whether to return detailed information about each space (default) or only the space names.
631
+
632
+ Returns:
633
+ Union[List[str], List[SpaceInformation]]: either a list of SpaceInformation objects (default) or a list of names.
634
+
635
+ Raises:
636
+ ValueError: If an invalid permission string is included in permissions.
637
+
638
+ """
639
+ if permissions and isinstance(permissions, Iterable):
640
+ for permission in permissions:
641
+ if permission.upper() not in AVAILABLE_PERMISSIONS:
642
+ raise ValueError(f"Invalid permission '{permission}'")
643
+ response = self._check_response(
644
+ self._kg_client.spaces.list(permissions=bool(permissions), pagination=Pagination(start=0, size=50))
645
+ )
646
+ accessible_spaces = list(response.items()) # makes additional requests if multiple pages of results
647
+ if permissions and isinstance(permissions, Iterable):
648
+ filtered_spaces = []
649
+ for space in accessible_spaces:
650
+ for permission in permissions:
651
+ if permission.upper() in space.permissions:
652
+ if names_only:
653
+ filtered_spaces.append(space.name)
654
+ else:
655
+ filtered_spaces.append(space)
656
+ return filtered_spaces
657
+ else:
658
+ if names_only:
659
+ return [space.name for space in accessible_spaces]
660
+ else:
661
+ return accessible_spaces
662
+
663
+ @property
664
+ def _private_space(self) -> str:
665
+ # temporary workaround
666
+ return f"private-{self.user_info().identifiers[0]}"
667
+
668
+ def configure_space(self, space_name: Optional[str] = None, types: Optional[List[KGObject]] = None) -> str:
669
+ """
670
+ Creates and configures a Knowledge Graph (KG) space with the specified name and types.
671
+
672
+ Args:
673
+ space_name (str, required (optional only if you run inside a collab)): The name of the KG space to create and configure.
674
+ If not provided, the method will try to obtain the collab ID from the environment
675
+ variables and use it to generate a default space name in the format "collab-collab_id".
676
+ If you are not launching this from inside an EBRAINS collab, you should provide a space name.
677
+ types (list of Type, required): An array containing the Type classes that should be included
678
+ in this space.
679
+
680
+ Returns:
681
+ str: The name of the configured KG space.
682
+
683
+ Example usage:
684
+ types = [Dataset, DatasetVersion, Software, SoftwareVersion]
685
+ space_name = "collab-MyCollab"
686
+ kg_client = KGClient()
687
+ kg_client.configure_space(space_name, types)
688
+ """
689
+ if space_name is None:
690
+ collab_id = os.environ.get("LAB_COLLAB_ID")
691
+ if collab_id is None:
692
+ raise ValueError(
693
+ "If you are not launching this from inside an EBRAINS collab, you should provide a space name with the following format: collab-collab_id."
694
+ )
695
+ else:
696
+ space_name = f"collab-{collab_id}"
697
+ result = self._kg_admin_client.create_space_definition(space=space_name)
698
+ if result: # error
699
+ err_msg = f"Unable to configure KG space for space '{space_name}': {result}"
700
+ if not space_name.startswith("collab-"):
701
+ err_msg += (
702
+ f". If you are trying to configure a collab space, ensure the space name starts with 'collab-'"
703
+ )
704
+ raise Exception(err_msg)
705
+ for cls in types:
706
+ if self.migrated:
707
+ target_type = cls.type_
708
+ else:
709
+ target_type = adapt_type_4to3(cls.type_)
710
+ result = self._kg_admin_client.assign_type_to_space(space=space_name, target_type=target_type)
711
+ if result: # error
712
+ raise Exception(f"Unable to assign {cls.__name__} to space {space_name}: {result}")
713
+ return space_name
714
+
715
+ def move_to_space(self, uri: str, destination_space: str):
716
+ """
717
+ Move a KG instance from one space to another.
718
+
719
+ This is not recursive, i.e. child instances much be moved individually.
720
+ """
721
+ # todo: add recursion
722
+ response = self._kg_client.instances.move(instance_id=self.uuid_from_uri(uri), space=destination_space)
723
+ if response.error:
724
+ raise Exception(response.error)
725
+
726
+ def space_info(
727
+ self,
728
+ space_name: str,
729
+ release_status: str = "released",
730
+ scope: Optional[str] = None,
731
+ ignore_errors: bool = False,
732
+ ):
733
+ """
734
+ Return information about the types and number of instances in a space.
735
+
736
+ The return format is a dictionary whose keys are classes and the values are the
737
+ number of instances of each class in the given spaces.
738
+ """
739
+ release_status = handle_scope_keyword(scope, release_status)
740
+ # todo: if not self.migrated, adapt type before lookup
741
+ result = self._kg_client.types.list(space=space_name, stage=STAGE_MAP[release_status])
742
+ if result.error:
743
+ raise Exception(result.error)
744
+ response = {}
745
+ for item in result.data:
746
+ if self.migrated:
747
+ type_iri = item.identifier
748
+ else:
749
+ type_ = {"@type": item.identifier}
750
+ adapt_namespaces_3to4(type_)
751
+ type_iri = type_["@type"]
752
+ try:
753
+ cls = lookup_type(type_iri, OPENMINDS_VERSION)
754
+ except (KeyError, ValueError) as err:
755
+ ignore_list = [
756
+ "https://core.kg.ebrains.eu/vocab/type/Bookmark",
757
+ "https://core.kg.ebrains.eu/vocab/meta/type/Query",
758
+ "https://openminds.om-i.org/types/Query",
759
+ "https://openminds.ebrains.eu/core/URL",
760
+ "https://openminds.om-i.org/types/URL"
761
+ ]
762
+ if ignore_errors or any(ignore in str(err) for ignore in ignore_list):
763
+ pass
764
+ else:
765
+ raise
766
+ else:
767
+ response[cls] = item.occurrences
768
+ return response
769
+
770
+ def clean_space(self, space_name):
771
+ """Delete all instances from a given space."""
772
+ # todo: check for released instances, they must be unreleased
773
+ # before deletion.
774
+ space_info = self.space_info(space_name, release_status="in progress", ignore_errors=True)
775
+ if sum(space_info.values()) > 0:
776
+ print(f"The space '{space_name}' contains the following instances:\n")
777
+ for cls, count in space_info.items():
778
+ if count > 0:
779
+ print(cls.__name__, count)
780
+ response = input("\nAre you sure you want to delete them? ")
781
+ if response not in ("y", "Y", "yes", "YES"):
782
+ return
783
+ error_messages = []
784
+ for cls, count in space_info.items():
785
+ if count > 0 and hasattr(cls, "list"): # exclude embedded metadata instances
786
+ print(f"Deleting {cls.__name__} instances", end=" ")
787
+ response = self.list(cls.type_, release_status="in progress", space=space_name, size=count)
788
+ assert response.total <= count
789
+ for instance in response.data:
790
+ assert instance["https://core.kg.ebrains.eu/vocab/meta/space"] == space_name
791
+ error = self.delete_instance(self.uuid_from_uri(instance["@id"]), ignore_not_found=False)
792
+ if error:
793
+ print("x", end="")
794
+ error_messages.append(error)
795
+ else:
796
+ print(".", end="")
797
+ print()
798
+ if error_messages:
799
+ print(error_messages)
800
+ else:
801
+ print(f"The space '{space_name}' is already clean")
802
+
803
+ def move_all_to_space(self, source_space: str, destination_space: str):
804
+ """
805
+ Move all the KG instances in one space to another.
806
+ """
807
+ assert source_space != destination_space
808
+ space_info = self.space_info(source_space, release_status="in progress")
809
+ if sum(space_info.values()) > 0:
810
+ print(f"The space '{source_space}' contains the following instances:\n")
811
+ for cls, count in space_info.items():
812
+ if count > 0:
813
+ print(cls.__name__, count)
814
+ response = input(f"\nAre you sure you want to move them to space '{destination_space}'? ")
815
+ if response not in ("y", "Y", "yes", "YES"):
816
+ return
817
+ for cls, count in space_info.items():
818
+ if count > 0 and hasattr(cls, "list"): # exclude embedded metadata instances
819
+ print(f"Moving {cls.__name__} instances", end="")
820
+ instances = cls.list(self, release_status="in progress", space=source_space)
821
+ assert len(instances) <= count
822
+ for instance in instances:
823
+ assert instance.space == source_space
824
+ print(".", end="")
825
+ self.move_to_space(instance.id, destination_space)
826
+ print()
827
+ else:
828
+ print(f"The space '{source_space}' is empty, nothing to move.")
829
+
830
+ def is_released(self, uri: str, with_children: bool = False) -> bool:
831
+ """
832
+ Release status of a KG instance identified by its URI.
833
+
834
+ Args:
835
+ uri (URI): persistent identifier of the instance.
836
+ with_children (bool): whether to check if all the children of the instance
837
+ have also been released.
838
+
839
+ Returns:
840
+ True if the instance and (optionally) all its children have been released.
841
+ Otherwise False.
842
+ """
843
+ if with_children:
844
+ release_tree_scope = ReleaseTreeScope.CHILDREN_ONLY
845
+ else:
846
+ release_tree_scope = ReleaseTreeScope.TOP_INSTANCE_ONLY
847
+ response = self._kg_client.instances.get_release_status(
848
+ instance_id=self.uuid_from_uri(uri), release_tree_scope=release_tree_scope
849
+ )
850
+ if response.data in ("RELEASED", "HAS_CHANGED"):
851
+ return True
852
+ elif response.data == "UNRELEASED":
853
+ return False
854
+ else:
855
+ raise AuthorizationError("You are not able to access the release status")
856
+
857
+ def release(self, uri: str):
858
+ """Release the instance with the given uri"""
859
+ response = self._kg_client.instances.release(self.uuid_from_uri(uri))
860
+ if response:
861
+ raise Exception(f"Can't release instance with id {uri}. Error message: {response}")
862
+
863
+ def unrelease(self, uri: str):
864
+ """Unrelease the instance with the given uri"""
865
+ response = self._kg_client.instances.unrelease(self.uuid_from_uri(uri))
866
+ if response:
867
+ raise Exception(f"Can't unrelease instance with id {uri}. Error message: {response}")