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.
- fairgraph/__init__.py +61 -0
- fairgraph/base.py +104 -0
- fairgraph/caching.py +52 -0
- fairgraph/client.py +867 -0
- fairgraph/collection.py +104 -0
- fairgraph/embedded.py +122 -0
- fairgraph/errors.py +47 -0
- fairgraph/fields.py +11 -0
- fairgraph/kgobject.py +1087 -0
- fairgraph/kgproxy.py +178 -0
- fairgraph/kgquery.py +151 -0
- fairgraph/node.py +488 -0
- fairgraph/openminds/__init__.py +1 -0
- fairgraph/openminds/chemicals/__init__.py +40 -0
- fairgraph/openminds/chemicals/amount_of_chemical.py +23 -0
- fairgraph/openminds/chemicals/chemical_mixture.py +72 -0
- fairgraph/openminds/chemicals/chemical_substance.py +67 -0
- fairgraph/openminds/chemicals/product_source.py +57 -0
- fairgraph/openminds/computation/__init__.py +53 -0
- fairgraph/openminds/computation/data_analysis.py +104 -0
- fairgraph/openminds/computation/data_copy.py +104 -0
- fairgraph/openminds/computation/environment.py +66 -0
- fairgraph/openminds/computation/generic_computation.py +104 -0
- fairgraph/openminds/computation/hardware_system.py +53 -0
- fairgraph/openminds/computation/launch_configuration.py +68 -0
- fairgraph/openminds/computation/local_file.py +83 -0
- fairgraph/openminds/computation/model_validation.py +107 -0
- fairgraph/openminds/computation/optimization.py +104 -0
- fairgraph/openminds/computation/simulation.py +104 -0
- fairgraph/openminds/computation/software_agent.py +81 -0
- fairgraph/openminds/computation/validation_test.py +105 -0
- fairgraph/openminds/computation/validation_test_version.py +180 -0
- fairgraph/openminds/computation/visualization.py +104 -0
- fairgraph/openminds/computation/workflow_execution.py +44 -0
- fairgraph/openminds/computation/workflow_recipe.py +95 -0
- fairgraph/openminds/computation/workflow_recipe_version.py +185 -0
- fairgraph/openminds/controlled_terms/__init__.py +116 -0
- fairgraph/openminds/controlled_terms/action_status_type.py +97 -0
- fairgraph/openminds/controlled_terms/age_category.py +89 -0
- fairgraph/openminds/controlled_terms/analysis_technique.py +108 -0
- fairgraph/openminds/controlled_terms/anatomical_axes_orientation.py +89 -0
- fairgraph/openminds/controlled_terms/anatomical_identification_type.py +89 -0
- fairgraph/openminds/controlled_terms/anatomical_plane.py +89 -0
- fairgraph/openminds/controlled_terms/annotation_criteria_type.py +89 -0
- fairgraph/openminds/controlled_terms/annotation_type.py +89 -0
- fairgraph/openminds/controlled_terms/atlas_type.py +88 -0
- fairgraph/openminds/controlled_terms/auditory_stimulus_type.py +127 -0
- fairgraph/openminds/controlled_terms/biological_order.py +117 -0
- fairgraph/openminds/controlled_terms/biological_process.py +79 -0
- fairgraph/openminds/controlled_terms/biological_sex.py +132 -0
- fairgraph/openminds/controlled_terms/breeding_type.py +127 -0
- fairgraph/openminds/controlled_terms/cell_culture_type.py +117 -0
- fairgraph/openminds/controlled_terms/cell_type.py +151 -0
- fairgraph/openminds/controlled_terms/chemical_mixture_type.py +89 -0
- fairgraph/openminds/controlled_terms/colormap.py +79 -0
- fairgraph/openminds/controlled_terms/contribution_type.py +79 -0
- fairgraph/openminds/controlled_terms/cranial_window_construction_type.py +89 -0
- fairgraph/openminds/controlled_terms/cranial_window_reinforcement_type.py +89 -0
- fairgraph/openminds/controlled_terms/criteria_quality_type.py +89 -0
- fairgraph/openminds/controlled_terms/data_type.py +89 -0
- fairgraph/openminds/controlled_terms/device_type.py +94 -0
- fairgraph/openminds/controlled_terms/difference_measure.py +89 -0
- fairgraph/openminds/controlled_terms/disease.py +142 -0
- fairgraph/openminds/controlled_terms/disease_model.py +142 -0
- fairgraph/openminds/controlled_terms/educational_level.py +79 -0
- fairgraph/openminds/controlled_terms/electrical_stimulus_type.py +137 -0
- fairgraph/openminds/controlled_terms/ethics_assessment.py +79 -0
- fairgraph/openminds/controlled_terms/experimental_approach.py +79 -0
- fairgraph/openminds/controlled_terms/file_bundle_grouping.py +99 -0
- fairgraph/openminds/controlled_terms/file_repository_type.py +89 -0
- fairgraph/openminds/controlled_terms/file_usage_role.py +89 -0
- fairgraph/openminds/controlled_terms/genetic_strain_type.py +127 -0
- fairgraph/openminds/controlled_terms/gustatory_stimulus_type.py +127 -0
- fairgraph/openminds/controlled_terms/handedness.py +127 -0
- fairgraph/openminds/controlled_terms/language.py +88 -0
- fairgraph/openminds/controlled_terms/laterality.py +94 -0
- fairgraph/openminds/controlled_terms/learning_resource_type.py +88 -0
- fairgraph/openminds/controlled_terms/measured_quantity.py +89 -0
- fairgraph/openminds/controlled_terms/measured_signal_type.py +79 -0
- fairgraph/openminds/controlled_terms/meta_data_model_type.py +88 -0
- fairgraph/openminds/controlled_terms/model_abstraction_level.py +89 -0
- fairgraph/openminds/controlled_terms/model_scope.py +89 -0
- fairgraph/openminds/controlled_terms/molecular_entity.py +142 -0
- fairgraph/openminds/controlled_terms/mri_pulse_sequence.py +98 -0
- fairgraph/openminds/controlled_terms/mri_weighting.py +98 -0
- fairgraph/openminds/controlled_terms/olfactory_stimulus_type.py +127 -0
- fairgraph/openminds/controlled_terms/operating_device.py +79 -0
- fairgraph/openminds/controlled_terms/operating_system.py +88 -0
- fairgraph/openminds/controlled_terms/optical_stimulus_type.py +127 -0
- fairgraph/openminds/controlled_terms/organ.py +161 -0
- fairgraph/openminds/controlled_terms/organism_substance.py +151 -0
- fairgraph/openminds/controlled_terms/organism_system.py +117 -0
- fairgraph/openminds/controlled_terms/patch_clamp_variation.py +89 -0
- fairgraph/openminds/controlled_terms/preparation_type.py +98 -0
- fairgraph/openminds/controlled_terms/product_accessibility.py +79 -0
- fairgraph/openminds/controlled_terms/programming_language.py +88 -0
- fairgraph/openminds/controlled_terms/qualitative_overlap.py +79 -0
- fairgraph/openminds/controlled_terms/semantic_data_type.py +79 -0
- fairgraph/openminds/controlled_terms/service.py +89 -0
- fairgraph/openminds/controlled_terms/setup_type.py +89 -0
- fairgraph/openminds/controlled_terms/software_application_category.py +79 -0
- fairgraph/openminds/controlled_terms/software_feature.py +79 -0
- fairgraph/openminds/controlled_terms/species.py +143 -0
- fairgraph/openminds/controlled_terms/stimulation_approach.py +98 -0
- fairgraph/openminds/controlled_terms/stimulation_technique.py +98 -0
- fairgraph/openminds/controlled_terms/subcellular_entity.py +143 -0
- fairgraph/openminds/controlled_terms/subject_attribute.py +89 -0
- fairgraph/openminds/controlled_terms/tactile_stimulus_type.py +127 -0
- fairgraph/openminds/controlled_terms/technique.py +108 -0
- fairgraph/openminds/controlled_terms/term_suggestion.py +121 -0
- fairgraph/openminds/controlled_terms/terminology.py +89 -0
- fairgraph/openminds/controlled_terms/tissue_sample_attribute.py +89 -0
- fairgraph/openminds/controlled_terms/tissue_sample_type.py +127 -0
- fairgraph/openminds/controlled_terms/type_of_uncertainty.py +89 -0
- fairgraph/openminds/controlled_terms/uberon_parcellation.py +153 -0
- fairgraph/openminds/controlled_terms/unit_of_measurement.py +108 -0
- fairgraph/openminds/controlled_terms/visual_stimulus_type.py +127 -0
- fairgraph/openminds/controlledterms.py +6 -0
- fairgraph/openminds/core/__init__.py +107 -0
- fairgraph/openminds/core/actors/__init__.py +7 -0
- fairgraph/openminds/core/actors/account_information.py +44 -0
- fairgraph/openminds/core/actors/affiliation.py +30 -0
- fairgraph/openminds/core/actors/consortium.py +175 -0
- fairgraph/openminds/core/actors/contact_information.py +43 -0
- fairgraph/openminds/core/actors/contribution.py +23 -0
- fairgraph/openminds/core/actors/organization.py +199 -0
- fairgraph/openminds/core/actors/person.py +236 -0
- fairgraph/openminds/core/data/__init__.py +13 -0
- fairgraph/openminds/core/data/content_type.py +107 -0
- fairgraph/openminds/core/data/content_type_pattern.py +53 -0
- fairgraph/openminds/core/data/copyright.py +23 -0
- fairgraph/openminds/core/data/file.py +275 -0
- fairgraph/openminds/core/data/file_archive.py +71 -0
- fairgraph/openminds/core/data/file_bundle.py +150 -0
- fairgraph/openminds/core/data/file_path_pattern.py +23 -0
- fairgraph/openminds/core/data/file_repository.py +99 -0
- fairgraph/openminds/core/data/file_repository_structure.py +51 -0
- fairgraph/openminds/core/data/hash.py +23 -0
- fairgraph/openminds/core/data/license.py +77 -0
- fairgraph/openminds/core/data/measurement.py +45 -0
- fairgraph/openminds/core/data/service_link.py +49 -0
- fairgraph/openminds/core/digital_identifier/__init__.py +11 -0
- fairgraph/openminds/core/digital_identifier/doi.py +98 -0
- fairgraph/openminds/core/digital_identifier/gridid.py +41 -0
- fairgraph/openminds/core/digital_identifier/handle.py +52 -0
- fairgraph/openminds/core/digital_identifier/identifiers_dot_org_id.py +41 -0
- fairgraph/openminds/core/digital_identifier/isbn.py +88 -0
- fairgraph/openminds/core/digital_identifier/issn.py +63 -0
- fairgraph/openminds/core/digital_identifier/orcid.py +41 -0
- fairgraph/openminds/core/digital_identifier/rorid.py +41 -0
- fairgraph/openminds/core/digital_identifier/rrid.py +55 -0
- fairgraph/openminds/core/digital_identifier/stock_number.py +23 -0
- fairgraph/openminds/core/digital_identifier/swhid.py +48 -0
- fairgraph/openminds/core/miscellaneous/__init__.py +7 -0
- fairgraph/openminds/core/miscellaneous/comment.py +47 -0
- fairgraph/openminds/core/miscellaneous/funding.py +70 -0
- fairgraph/openminds/core/miscellaneous/quantitative_value.py +43 -0
- fairgraph/openminds/core/miscellaneous/quantitative_value_array.py +49 -0
- fairgraph/openminds/core/miscellaneous/quantitative_value_range.py +43 -0
- fairgraph/openminds/core/miscellaneous/research_product_group.py +26 -0
- fairgraph/openminds/core/miscellaneous/web_resource.py +104 -0
- fairgraph/openminds/core/products/__init__.py +12 -0
- fairgraph/openminds/core/products/dataset.py +95 -0
- fairgraph/openminds/core/products/dataset_version.py +240 -0
- fairgraph/openminds/core/products/meta_data_model.py +95 -0
- fairgraph/openminds/core/products/meta_data_model_version.py +168 -0
- fairgraph/openminds/core/products/model.py +103 -0
- fairgraph/openminds/core/products/model_version.py +235 -0
- fairgraph/openminds/core/products/project.py +56 -0
- fairgraph/openminds/core/products/setup.py +69 -0
- fairgraph/openminds/core/products/software.py +95 -0
- fairgraph/openminds/core/products/software_version.py +226 -0
- fairgraph/openminds/core/products/web_service.py +103 -0
- fairgraph/openminds/core/products/web_service_version.py +182 -0
- fairgraph/openminds/core/research/__init__.py +17 -0
- fairgraph/openminds/core/research/behavioral_protocol.py +69 -0
- fairgraph/openminds/core/research/configuration.py +67 -0
- fairgraph/openminds/core/research/custom_property_set.py +27 -0
- fairgraph/openminds/core/research/numerical_property.py +23 -0
- fairgraph/openminds/core/research/property_value_list.py +71 -0
- fairgraph/openminds/core/research/protocol.py +67 -0
- fairgraph/openminds/core/research/protocol_execution.py +76 -0
- fairgraph/openminds/core/research/strain.py +90 -0
- fairgraph/openminds/core/research/string_property.py +23 -0
- fairgraph/openminds/core/research/subject.py +79 -0
- fairgraph/openminds/core/research/subject_group.py +91 -0
- fairgraph/openminds/core/research/subject_group_state.py +113 -0
- fairgraph/openminds/core/research/subject_state.py +138 -0
- fairgraph/openminds/core/research/tissue_sample.py +87 -0
- fairgraph/openminds/core/research/tissue_sample_collection.py +99 -0
- fairgraph/openminds/core/research/tissue_sample_collection_state.py +109 -0
- fairgraph/openminds/core/research/tissue_sample_state.py +127 -0
- fairgraph/openminds/ephys/__init__.py +39 -0
- fairgraph/openminds/ephys/activity/__init__.py +3 -0
- fairgraph/openminds/ephys/activity/cell_patching.py +73 -0
- fairgraph/openminds/ephys/activity/electrode_placement.py +67 -0
- fairgraph/openminds/ephys/activity/recording_activity.py +67 -0
- fairgraph/openminds/ephys/device/__init__.py +6 -0
- fairgraph/openminds/ephys/device/electrode.py +81 -0
- fairgraph/openminds/ephys/device/electrode_array.py +85 -0
- fairgraph/openminds/ephys/device/electrode_array_usage.py +105 -0
- fairgraph/openminds/ephys/device/electrode_usage.py +101 -0
- fairgraph/openminds/ephys/device/pipette.py +81 -0
- fairgraph/openminds/ephys/device/pipette_usage.py +123 -0
- fairgraph/openminds/ephys/entity/__init__.py +2 -0
- fairgraph/openminds/ephys/entity/channel.py +23 -0
- fairgraph/openminds/ephys/entity/recording.py +63 -0
- fairgraph/openminds/publications/__init__.py +47 -0
- fairgraph/openminds/publications/book.py +106 -0
- fairgraph/openminds/publications/chapter.py +100 -0
- fairgraph/openminds/publications/learning_resource.py +90 -0
- fairgraph/openminds/publications/live_paper.py +95 -0
- fairgraph/openminds/publications/live_paper_resource_item.py +58 -0
- fairgraph/openminds/publications/live_paper_section.py +57 -0
- fairgraph/openminds/publications/live_paper_version.py +177 -0
- fairgraph/openminds/publications/periodical.py +53 -0
- fairgraph/openminds/publications/publication_issue.py +44 -0
- fairgraph/openminds/publications/publication_volume.py +44 -0
- fairgraph/openminds/publications/scholarly_article.py +146 -0
- fairgraph/openminds/sands/__init__.py +57 -0
- fairgraph/openminds/sands/atlas/__init__.py +9 -0
- fairgraph/openminds/sands/atlas/atlas_annotation.py +52 -0
- fairgraph/openminds/sands/atlas/brain_atlas.py +113 -0
- fairgraph/openminds/sands/atlas/brain_atlas_version.py +213 -0
- fairgraph/openminds/sands/atlas/common_coordinate_space.py +121 -0
- fairgraph/openminds/sands/atlas/common_coordinate_space_version.py +243 -0
- fairgraph/openminds/sands/atlas/parcellation_entity.py +133 -0
- fairgraph/openminds/sands/atlas/parcellation_entity_version.py +162 -0
- fairgraph/openminds/sands/atlas/parcellation_terminology.py +38 -0
- fairgraph/openminds/sands/atlas/parcellation_terminology_version.py +38 -0
- fairgraph/openminds/sands/mathematical_shapes/__init__.py +3 -0
- fairgraph/openminds/sands/mathematical_shapes/circle.py +23 -0
- fairgraph/openminds/sands/mathematical_shapes/ellipse.py +27 -0
- fairgraph/openminds/sands/mathematical_shapes/rectangle.py +23 -0
- fairgraph/openminds/sands/miscellaneous/__init__.py +6 -0
- fairgraph/openminds/sands/miscellaneous/anatomical_target_position.py +40 -0
- fairgraph/openminds/sands/miscellaneous/coordinate_point.py +23 -0
- fairgraph/openminds/sands/miscellaneous/qualitative_relation_assessment.py +34 -0
- fairgraph/openminds/sands/miscellaneous/quantitative_relation_assessment.py +38 -0
- fairgraph/openminds/sands/miscellaneous/single_color.py +24 -0
- fairgraph/openminds/sands/miscellaneous/viewer_specification.py +40 -0
- fairgraph/openminds/sands/non_atlas/__init__.py +3 -0
- fairgraph/openminds/sands/non_atlas/custom_anatomical_entity.py +110 -0
- fairgraph/openminds/sands/non_atlas/custom_annotation.py +54 -0
- fairgraph/openminds/sands/non_atlas/custom_coordinate_space.py +67 -0
- fairgraph/openminds/specimen_prep/__init__.py +38 -0
- fairgraph/openminds/specimen_prep/activity/__init__.py +3 -0
- fairgraph/openminds/specimen_prep/activity/cranial_window_preparation.py +69 -0
- fairgraph/openminds/specimen_prep/activity/tissue_culture_preparation.py +67 -0
- fairgraph/openminds/specimen_prep/activity/tissue_sample_slicing.py +69 -0
- fairgraph/openminds/specimen_prep/device/__init__.py +2 -0
- fairgraph/openminds/specimen_prep/device/slicing_device.py +73 -0
- fairgraph/openminds/specimen_prep/device/slicing_device_usage.py +117 -0
- fairgraph/openminds/specimenprep.py +4 -0
- fairgraph/openminds/stimulation/__init__.py +38 -0
- fairgraph/openminds/stimulation/activity/__init__.py +1 -0
- fairgraph/openminds/stimulation/activity/stimulation_activity.py +67 -0
- fairgraph/openminds/stimulation/stimulus/__init__.py +1 -0
- fairgraph/openminds/stimulation/stimulus/ephys_stimulus.py +63 -0
- fairgraph/queries.py +499 -0
- fairgraph/registry.py +123 -0
- fairgraph/utility.py +607 -0
- fairgraph-0.13.0.dist-info/METADATA +222 -0
- fairgraph-0.13.0.dist-info/RECORD +267 -0
- fairgraph-0.13.0.dist-info/WHEEL +5 -0
- fairgraph-0.13.0.dist-info/licenses/LICENSE.txt +177 -0
- fairgraph-0.13.0.dist-info/top_level.txt +1 -0
fairgraph/kgobject.py
ADDED
|
@@ -0,0 +1,1087 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This module provides the KGObject class, which is the base class
|
|
3
|
+
for representations of structured metadata that have a globally
|
|
4
|
+
unique identifier (a URI).
|
|
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 collections import defaultdict
|
|
23
|
+
import logging
|
|
24
|
+
from uuid import UUID
|
|
25
|
+
from warnings import warn
|
|
26
|
+
from typing import Any, Tuple, Dict, List, Optional, TYPE_CHECKING, Union
|
|
27
|
+
|
|
28
|
+
from requests.exceptions import HTTPError, ConnectionError
|
|
29
|
+
|
|
30
|
+
try:
|
|
31
|
+
from tabulate import tabulate
|
|
32
|
+
|
|
33
|
+
have_tabulate = True
|
|
34
|
+
except ImportError:
|
|
35
|
+
have_tabulate = False
|
|
36
|
+
|
|
37
|
+
from openminds.registry import lookup_type
|
|
38
|
+
from openminds import IRI, LinkedMetadata
|
|
39
|
+
|
|
40
|
+
from .utility import expand_uri, as_list, expand_filter, ActivityLog, normalize_data, handle_scope_keyword
|
|
41
|
+
from .queries import Query, QueryProperty
|
|
42
|
+
from .errors import AuthorizationError, ResourceExistsError, CannotBuildExistenceQuery
|
|
43
|
+
from .caching import object_cache, save_cache, generate_cache_key
|
|
44
|
+
from .base import RepresentsSingleObject, SupportsQuerying, JSONdict, OPENMINDS_VERSION
|
|
45
|
+
from .node import ContainsMetadata
|
|
46
|
+
from .kgproxy import KGProxy
|
|
47
|
+
from .kgquery import KGQuery
|
|
48
|
+
|
|
49
|
+
if TYPE_CHECKING:
|
|
50
|
+
from .properties import Property
|
|
51
|
+
from .client import KGClient
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
logger = logging.getLogger("fairgraph")
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class KGObject(ContainsMetadata, RepresentsSingleObject, SupportsQuerying):
|
|
58
|
+
"""
|
|
59
|
+
Base class for Knowledge Graph objects.
|
|
60
|
+
|
|
61
|
+
Should not be instantiated directly, intended to be subclassed.
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
existence_query_properties: Tuple[str, ...] = ("name",)
|
|
65
|
+
# Note that this default value of existence_query_properties should in
|
|
66
|
+
# many cases be over-ridden.
|
|
67
|
+
# It assumes that "name" is unique within instances of a given type,
|
|
68
|
+
# which may often not be the case.
|
|
69
|
+
|
|
70
|
+
def __init__(
|
|
71
|
+
self,
|
|
72
|
+
id: Optional[str] = None,
|
|
73
|
+
data: Optional[JSONdict] = None,
|
|
74
|
+
space: Optional[str] = None,
|
|
75
|
+
release_status: Optional[str] = None,
|
|
76
|
+
**properties,
|
|
77
|
+
):
|
|
78
|
+
self.id = id
|
|
79
|
+
self._space = space
|
|
80
|
+
self.release_status = release_status
|
|
81
|
+
self.allow_update = True
|
|
82
|
+
super().__init__(data=data, **properties)
|
|
83
|
+
for prop in self.reverse_properties:
|
|
84
|
+
if not hasattr(self, prop.name):
|
|
85
|
+
query = KGQuery(
|
|
86
|
+
prop.types, {prop.reverse: self.id}, callback=lambda value: setattr(self, prop.name, value)
|
|
87
|
+
)
|
|
88
|
+
setattr(self, prop.name, query)
|
|
89
|
+
|
|
90
|
+
self._raw_remote_data = None
|
|
91
|
+
self.remote_data = {}
|
|
92
|
+
if self.id and self.id.startswith("http"):
|
|
93
|
+
# we store the original remote data in `_raw_remote_data`
|
|
94
|
+
# and a normalized version in `remote_data`
|
|
95
|
+
self._raw_remote_data = data # for debugging
|
|
96
|
+
if data:
|
|
97
|
+
self.remote_data = normalize_data(
|
|
98
|
+
self.to_jsonld(include_empty_properties=False, embed_linked_nodes=False),
|
|
99
|
+
data.get("@context", self.context)
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
def __repr__(self):
|
|
103
|
+
template_parts = (
|
|
104
|
+
"{}={{self.{}!r}}".format(prop.name, prop.name)
|
|
105
|
+
for prop in self.__class__.all_properties
|
|
106
|
+
if getattr(self, prop.name, None) is not None
|
|
107
|
+
)
|
|
108
|
+
template = "{self.__class__.__name__}(" + ", ".join(template_parts) + ", space={self.space}, id={self.id})"
|
|
109
|
+
return template.format(self=self)
|
|
110
|
+
|
|
111
|
+
@property
|
|
112
|
+
def space(self) -> Union[str, None]:
|
|
113
|
+
if self._raw_remote_data:
|
|
114
|
+
if "https://schema.hbp.eu/myQuery/space" in self._raw_remote_data:
|
|
115
|
+
self._space = self._raw_remote_data["https://schema.hbp.eu/myQuery/space"]
|
|
116
|
+
elif "https://core.kg.ebrains.eu/vocab/meta/space" in self._raw_remote_data:
|
|
117
|
+
self._space = self._raw_remote_data["https://core.kg.ebrains.eu/vocab/meta/space"]
|
|
118
|
+
return self._space
|
|
119
|
+
|
|
120
|
+
@classmethod
|
|
121
|
+
def from_jsonld(
|
|
122
|
+
cls,
|
|
123
|
+
data: JSONdict,
|
|
124
|
+
ignore_unexpected_keys: Optional[bool] = False,
|
|
125
|
+
release_status: Optional[str] = None
|
|
126
|
+
) -> KGObject:
|
|
127
|
+
"""Create an instance of the class from a JSON-LD document."""
|
|
128
|
+
# todo: handle ignore_unexpected_keys
|
|
129
|
+
deserialized_data = cls._deserialize_data(data, include_id=True)
|
|
130
|
+
return cls(id=data.get("@id", None), data=data, release_status=release_status, **deserialized_data)
|
|
131
|
+
|
|
132
|
+
# @classmethod
|
|
133
|
+
# def _fix_keys(cls, data):
|
|
134
|
+
# """
|
|
135
|
+
# The KG Query API does not allow the same property name to be used twice in a document.
|
|
136
|
+
# This is a problem when resolving linked nodes which use the same property names
|
|
137
|
+
# as the 'parent'. As a workaround, we prefix the property names in the linked node
|
|
138
|
+
# with the class name.
|
|
139
|
+
# This method removes this prefix.
|
|
140
|
+
# This feels like a kludge, and I'd be happy to find a better solution.
|
|
141
|
+
# """
|
|
142
|
+
# prefix = cls.__name__ + "__"
|
|
143
|
+
# for key in list(data):
|
|
144
|
+
# # need to use list() in previous line to avoid
|
|
145
|
+
# # "dictionary keys changed during iteration" error in Python 3.8+
|
|
146
|
+
# if key.startswith(prefix):
|
|
147
|
+
# fixed_key = key.replace(prefix, "")
|
|
148
|
+
# data[fixed_key] = data.pop(key)
|
|
149
|
+
# return data
|
|
150
|
+
|
|
151
|
+
@classmethod
|
|
152
|
+
def from_uri(
|
|
153
|
+
cls,
|
|
154
|
+
uri: str,
|
|
155
|
+
client: KGClient,
|
|
156
|
+
use_cache: bool = True,
|
|
157
|
+
release_status: str = "released",
|
|
158
|
+
scope: Optional[str] = None,
|
|
159
|
+
follow_links: Optional[Dict[str, Any]] = None,
|
|
160
|
+
with_reverse_properties: Optional[bool] = False,
|
|
161
|
+
):
|
|
162
|
+
"""
|
|
163
|
+
Retrieve an instance from the Knowledge Graph based on its URI.
|
|
164
|
+
|
|
165
|
+
Args:
|
|
166
|
+
uri (str): long-form identifier for the KG instance (a full URI)
|
|
167
|
+
client: a KGClient
|
|
168
|
+
release_status (str, optional): The scope of the lookup. Valid values are "released", "in progress", or "any".
|
|
169
|
+
Defaults to "released".
|
|
170
|
+
use_cache (bool): Whether to use cached data if they exist. Defaults to True.
|
|
171
|
+
follow_links (dict): The links in the graph to follow. Defaults to None.
|
|
172
|
+
with_reverse_properties (bool): Whether to include reverse properties. Defaults to False.
|
|
173
|
+
"""
|
|
174
|
+
release_status = handle_scope_keyword(scope, release_status)
|
|
175
|
+
if follow_links:
|
|
176
|
+
query = cls.generate_query(
|
|
177
|
+
space=None,
|
|
178
|
+
client=client,
|
|
179
|
+
filters=None,
|
|
180
|
+
follow_links=follow_links,
|
|
181
|
+
with_reverse_properties=with_reverse_properties,
|
|
182
|
+
)
|
|
183
|
+
results = client.query(
|
|
184
|
+
query, instance_id=client.uuid_from_uri(uri), size=1, release_status=release_status
|
|
185
|
+
).data
|
|
186
|
+
if results:
|
|
187
|
+
data = results[0]
|
|
188
|
+
else:
|
|
189
|
+
data = None
|
|
190
|
+
else:
|
|
191
|
+
data = client.instance_from_full_uri(uri, use_cache=use_cache, release_status=release_status)
|
|
192
|
+
if data is None:
|
|
193
|
+
return None
|
|
194
|
+
else:
|
|
195
|
+
return cls.from_jsonld(data, release_status=release_status)
|
|
196
|
+
|
|
197
|
+
@classmethod
|
|
198
|
+
def from_uuid(
|
|
199
|
+
cls,
|
|
200
|
+
uuid: str,
|
|
201
|
+
client: KGClient,
|
|
202
|
+
use_cache: bool = True,
|
|
203
|
+
release_status: str = "released",
|
|
204
|
+
scope: Optional[str] = None,
|
|
205
|
+
follow_links: Optional[Dict[str, Any]] = None,
|
|
206
|
+
with_reverse_properties: Optional[bool] = False,
|
|
207
|
+
):
|
|
208
|
+
"""
|
|
209
|
+
Retrieve an instance from the Knowledge Graph based on its UUID.
|
|
210
|
+
|
|
211
|
+
Args:
|
|
212
|
+
uuid (str): short-form identifier for the KG instance (a UUID).
|
|
213
|
+
client: a KGClient
|
|
214
|
+
release_status (str, optional): The scope of the lookup. Valid values are "released", "in progress", or "any".
|
|
215
|
+
Defaults to "released".
|
|
216
|
+
use_cache (bool): Whether to use cached data if they exist. Defaults to True.
|
|
217
|
+
follow_links (dict): The links in the graph to follow. Defaults to None.
|
|
218
|
+
with_reverse_properties (bool): Whether to include reverse properties. Defaults to False.
|
|
219
|
+
|
|
220
|
+
"""
|
|
221
|
+
release_status = handle_scope_keyword(scope, release_status)
|
|
222
|
+
logger.info("Attempting to retrieve {} with uuid {}".format(cls.__name__, uuid))
|
|
223
|
+
if len(uuid) == 0:
|
|
224
|
+
raise ValueError("Empty UUID")
|
|
225
|
+
try:
|
|
226
|
+
val = UUID(uuid, version=4) # check validity of uuid
|
|
227
|
+
except ValueError as err:
|
|
228
|
+
raise ValueError("{} - {}".format(err, uuid))
|
|
229
|
+
uri = cls.uri_from_uuid(uuid, client)
|
|
230
|
+
return cls.from_uri(
|
|
231
|
+
uri,
|
|
232
|
+
client,
|
|
233
|
+
use_cache=use_cache,
|
|
234
|
+
release_status=release_status,
|
|
235
|
+
follow_links=follow_links,
|
|
236
|
+
with_reverse_properties=with_reverse_properties,
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
@classmethod
|
|
240
|
+
def from_id(
|
|
241
|
+
cls,
|
|
242
|
+
id: str,
|
|
243
|
+
client: KGClient,
|
|
244
|
+
use_cache: bool = True,
|
|
245
|
+
release_status: str = "released",
|
|
246
|
+
scope: Optional[str] = None,
|
|
247
|
+
follow_links: Optional[Dict[str, Any]] = None,
|
|
248
|
+
with_reverse_properties: Optional[bool] = False,
|
|
249
|
+
):
|
|
250
|
+
"""
|
|
251
|
+
Retrieve an instance from the Knowledge Graph based on either its URI or UUID.
|
|
252
|
+
|
|
253
|
+
Args:
|
|
254
|
+
id (str): short-form (UUID) or long-form (URI) identifier for the KG instance.
|
|
255
|
+
client: a KGClient
|
|
256
|
+
release_status (str, optional): The scope of the lookup. Valid values are "released", "in progress", or "any".
|
|
257
|
+
Defaults to "released".
|
|
258
|
+
use_cache (bool): Whether to use cached data if they exist. Defaults to True.
|
|
259
|
+
follow_links (dict): The links in the graph to follow. Defaults to None.
|
|
260
|
+
with_reverse_properties (bool): Whether to include reverse properties. Defaults to False.
|
|
261
|
+
|
|
262
|
+
Returns:
|
|
263
|
+
Either a KGObject of the correct type, or None.
|
|
264
|
+
A return value of None means either the object doesn't exist
|
|
265
|
+
or the user doesn't have permission to access it.
|
|
266
|
+
"""
|
|
267
|
+
release_status = handle_scope_keyword(scope, release_status)
|
|
268
|
+
if hasattr(cls, "type_") and cls.type_:
|
|
269
|
+
if id.startswith("http"):
|
|
270
|
+
fn = cls.from_uri
|
|
271
|
+
else:
|
|
272
|
+
fn = cls.from_uuid
|
|
273
|
+
return fn(
|
|
274
|
+
id,
|
|
275
|
+
client,
|
|
276
|
+
use_cache=use_cache,
|
|
277
|
+
release_status=release_status,
|
|
278
|
+
follow_links=follow_links,
|
|
279
|
+
with_reverse_properties=with_reverse_properties,
|
|
280
|
+
)
|
|
281
|
+
else:
|
|
282
|
+
# if we don't know the type
|
|
283
|
+
if id.startswith("http"):
|
|
284
|
+
uri = id
|
|
285
|
+
else:
|
|
286
|
+
uri = client.uri_from_uuid(id)
|
|
287
|
+
if follow_links is not None:
|
|
288
|
+
raise NotImplementedError
|
|
289
|
+
data = client.instance_from_full_uri(uri, use_cache=use_cache, release_status=release_status)
|
|
290
|
+
type_ = data["@type"]
|
|
291
|
+
if isinstance(type_, list):
|
|
292
|
+
assert len(type_) == 1
|
|
293
|
+
type_ = type_[0]
|
|
294
|
+
cls_from_data = lookup_type(type_, OPENMINDS_VERSION)
|
|
295
|
+
return cls_from_data.from_jsonld(data, release_status=release_status)
|
|
296
|
+
|
|
297
|
+
@classmethod
|
|
298
|
+
def from_alias(
|
|
299
|
+
cls,
|
|
300
|
+
alias: str,
|
|
301
|
+
client: KGClient,
|
|
302
|
+
space: Optional[str] = None,
|
|
303
|
+
release_status: str = "released",
|
|
304
|
+
scope: Optional[str] = None,
|
|
305
|
+
follow_links: Optional[Dict[str, Any]] = None,
|
|
306
|
+
):
|
|
307
|
+
"""
|
|
308
|
+
Retrieve an instance from the Knowledge Graph based on its alias/short name.
|
|
309
|
+
|
|
310
|
+
Note that not all metadata classes have an alias.
|
|
311
|
+
|
|
312
|
+
Args:
|
|
313
|
+
alias (str): a short name used to identify a KG instance.
|
|
314
|
+
client: a KGClient
|
|
315
|
+
space (str, optional): the KG space to look in. Default is to look in all available spaces.
|
|
316
|
+
release_status (str, optional): The scope of the lookup. Valid values are "released", "in progress", or "any".
|
|
317
|
+
Defaults to "released".
|
|
318
|
+
follow_links (dict): The links in the graph to follow. Defaults to None.
|
|
319
|
+
|
|
320
|
+
"""
|
|
321
|
+
release_status = handle_scope_keyword(scope, release_status)
|
|
322
|
+
# todo: move this to openminds generation, and include only in those subclasses
|
|
323
|
+
# that have an alias
|
|
324
|
+
# todo: also count 'lookup_name' as an alias
|
|
325
|
+
if "short_name" not in cls.property_names:
|
|
326
|
+
raise AttributeError(f"{cls.__name__} doesn't have an 'alias' or 'short_name' property")
|
|
327
|
+
candidates = as_list(
|
|
328
|
+
cls.list(
|
|
329
|
+
client,
|
|
330
|
+
size=20,
|
|
331
|
+
from_index=0,
|
|
332
|
+
api="query",
|
|
333
|
+
release_status=release_status,
|
|
334
|
+
space=space,
|
|
335
|
+
alias=alias,
|
|
336
|
+
follow_links=follow_links,
|
|
337
|
+
)
|
|
338
|
+
)
|
|
339
|
+
if len(candidates) == 0:
|
|
340
|
+
return None
|
|
341
|
+
elif len(candidates) == 1:
|
|
342
|
+
return candidates[0]
|
|
343
|
+
else: # KG query does a "contains" lookup, so can get multiple results
|
|
344
|
+
for candidate in candidates:
|
|
345
|
+
if candidate.alias == alias:
|
|
346
|
+
return candidate
|
|
347
|
+
warn(
|
|
348
|
+
"Multiple objects found with a similar alias, but none match exactly." "Returning the first one found."
|
|
349
|
+
)
|
|
350
|
+
return candidates[0]
|
|
351
|
+
|
|
352
|
+
@property
|
|
353
|
+
def uuid(self) -> Union[str, None]:
|
|
354
|
+
# todo: consider using client._kg_client.uuid_from_absolute_id
|
|
355
|
+
if self.id is not None:
|
|
356
|
+
value = self.id.split("/")[-1]
|
|
357
|
+
return str(UUID(value))
|
|
358
|
+
else:
|
|
359
|
+
return None
|
|
360
|
+
|
|
361
|
+
@classmethod
|
|
362
|
+
def uri_from_uuid(cls, uuid: str, client: KGClient) -> str:
|
|
363
|
+
"""Convert an instances short-form identifier (a UUID) into the long-form (a URI)"""
|
|
364
|
+
return client.uri_from_uuid(uuid)
|
|
365
|
+
|
|
366
|
+
@classmethod
|
|
367
|
+
def list(
|
|
368
|
+
cls,
|
|
369
|
+
client: KGClient,
|
|
370
|
+
size: int = 100,
|
|
371
|
+
from_index: int = 0,
|
|
372
|
+
api: str = "auto",
|
|
373
|
+
release_status: str = "released",
|
|
374
|
+
scope: Optional[str] = None,
|
|
375
|
+
space: Optional[str] = None,
|
|
376
|
+
follow_links: Optional[Dict[str, Any]] = None,
|
|
377
|
+
with_reverse_properties: Optional[bool] = False,
|
|
378
|
+
**filters,
|
|
379
|
+
) -> List[KGObject]:
|
|
380
|
+
"""
|
|
381
|
+
List all objects of this type in the Knowledge Graph
|
|
382
|
+
|
|
383
|
+
Args:
|
|
384
|
+
client: KGClient object that handles the communication with the KG.
|
|
385
|
+
size (int, optional): The maximum number of instances to return. Default is 100.
|
|
386
|
+
from_index (int, optional): The index of the first instance to return. Default is 0.
|
|
387
|
+
api (str): The KG API to use for the query. Can be 'query', 'core', or 'auto'. Default is 'auto'.
|
|
388
|
+
release_status (str, optional): The scope to use for the query. Can be 'released', 'in progress', or 'all'. Default is 'released'.
|
|
389
|
+
space (str, optional): The KG space to be queried. If not specified, results from all accessible spaces will be included.
|
|
390
|
+
follow_links (dict): The links in the graph to follow. Defaults to None.
|
|
391
|
+
with_reverse_properties (bool): Whether to include reverse properties. Defaults to False.
|
|
392
|
+
filters: Optional keyword arguments representing filters to apply to the query.
|
|
393
|
+
|
|
394
|
+
Returns:
|
|
395
|
+
A list of instances of this class representing the objects returned by the KG query.
|
|
396
|
+
|
|
397
|
+
Raises:
|
|
398
|
+
ValueError: If invalid arguments are passed to the method.
|
|
399
|
+
NotImplementedError: If 'follow_links' is used with api='core'.
|
|
400
|
+
|
|
401
|
+
Example:
|
|
402
|
+
|
|
403
|
+
>>> from fairgraph import KGClient
|
|
404
|
+
>>> import fairgraph.openminds.controlled_terms as terms
|
|
405
|
+
>>> interneuron_types = terms.CellType.list(client, name="interneuron")
|
|
406
|
+
>>> for ct in interneuron_types[:4]:
|
|
407
|
+
... print(f"{ct.name:<30} {ct.definition}")
|
|
408
|
+
cerebellar interneuron None
|
|
409
|
+
cholinergic interneuron An inhibitory interneuron which mainly uses the neurotrasmitter acetylcholine (ACh).
|
|
410
|
+
cortical interneuron None
|
|
411
|
+
fast spiking interneuron A parvalbumin positive GABAergic interneuron with a high-frequency firing pattern.
|
|
412
|
+
|
|
413
|
+
"""
|
|
414
|
+
release_status = handle_scope_keyword(scope, release_status)
|
|
415
|
+
|
|
416
|
+
if api == "auto":
|
|
417
|
+
if filters:
|
|
418
|
+
api = "query"
|
|
419
|
+
else:
|
|
420
|
+
api = "core"
|
|
421
|
+
|
|
422
|
+
if api == "query":
|
|
423
|
+
query = cls.generate_query(
|
|
424
|
+
space=space,
|
|
425
|
+
client=client,
|
|
426
|
+
filters=filters,
|
|
427
|
+
follow_links=follow_links,
|
|
428
|
+
with_reverse_properties=with_reverse_properties,
|
|
429
|
+
)
|
|
430
|
+
instances = client.query(
|
|
431
|
+
query=query,
|
|
432
|
+
from_index=from_index,
|
|
433
|
+
size=size,
|
|
434
|
+
release_status=release_status,
|
|
435
|
+
).data
|
|
436
|
+
elif api == "core":
|
|
437
|
+
if filters:
|
|
438
|
+
raise ValueError("Cannot use filters with api='core'")
|
|
439
|
+
if follow_links:
|
|
440
|
+
raise NotImplementedError("Following links with api='core' not yet implemented")
|
|
441
|
+
instances = client.list(
|
|
442
|
+
cls.type_, space=space, from_index=from_index, size=size, release_status=release_status
|
|
443
|
+
).data
|
|
444
|
+
else:
|
|
445
|
+
raise ValueError("'api' must be either 'query', 'core', or 'auto'")
|
|
446
|
+
return [cls.from_jsonld(data=instance, release_status=release_status) for instance in instances]
|
|
447
|
+
|
|
448
|
+
@classmethod
|
|
449
|
+
def count(
|
|
450
|
+
cls,
|
|
451
|
+
client: KGClient,
|
|
452
|
+
api: str = "auto",
|
|
453
|
+
release_status: str = "released",
|
|
454
|
+
scope: Optional[str] = None,
|
|
455
|
+
space: Optional[str] = None,
|
|
456
|
+
**filters,
|
|
457
|
+
) -> int:
|
|
458
|
+
"""
|
|
459
|
+
Count the number of objects of a given type and (optionally) matching a given set of filters.
|
|
460
|
+
|
|
461
|
+
Args:
|
|
462
|
+
client: KGClient object that handles the communication with the KG.
|
|
463
|
+
api (str): The KG API to use for the query. Can be 'query', 'core', or 'auto'. Default is 'auto'.
|
|
464
|
+
release_status (str, optional): The scope to use for the query. Can be 'released', 'in progress', or 'all'. Default is 'released'.
|
|
465
|
+
space (str, optional): The KG space to be queried. If not specified, results from all accessible spaces will be counted.
|
|
466
|
+
filters: Optional keyword arguments representing filters to apply to the query.
|
|
467
|
+
|
|
468
|
+
Returns:
|
|
469
|
+
The number of instances of this class in the given space that would match the given filters,
|
|
470
|
+
or the total number of instances if no filters are provided.
|
|
471
|
+
|
|
472
|
+
Raises:
|
|
473
|
+
ValueError: If invalid arguments are passed to the method.
|
|
474
|
+
NotImplementedError: If 'follow_links' is used with api='core'.
|
|
475
|
+
|
|
476
|
+
Example:
|
|
477
|
+
|
|
478
|
+
>>> from fairgraph import KGClient
|
|
479
|
+
>>> import fairgraph.openminds.controlled_terms as terms
|
|
480
|
+
>>> terms.CellType.count(client, name="interneuron")
|
|
481
|
+
8
|
|
482
|
+
|
|
483
|
+
"""
|
|
484
|
+
release_status = handle_scope_keyword(scope, release_status)
|
|
485
|
+
if api == "auto":
|
|
486
|
+
if filters:
|
|
487
|
+
api = "query"
|
|
488
|
+
else:
|
|
489
|
+
api = "core"
|
|
490
|
+
if api == "query":
|
|
491
|
+
query = cls.generate_query(space=space, client=client, filters=filters)
|
|
492
|
+
response = client.query(query=query, from_index=0, size=1, release_status=release_status)
|
|
493
|
+
elif api == "core":
|
|
494
|
+
if filters:
|
|
495
|
+
raise ValueError("Cannot use filters with api='core'")
|
|
496
|
+
response = client.list(cls.type_, space=space, release_status=release_status, from_index=0, size=1)
|
|
497
|
+
return response.total
|
|
498
|
+
|
|
499
|
+
def _update_empty_properties(self, data: JSONdict):
|
|
500
|
+
"""Replace any empty properties (value None) with the supplied data"""
|
|
501
|
+
cls = self.__class__
|
|
502
|
+
deserialized_data = cls._deserialize_data(data, include_id=True)
|
|
503
|
+
for prop in cls.all_properties:
|
|
504
|
+
current_value = getattr(self, prop.name, None)
|
|
505
|
+
if current_value is None:
|
|
506
|
+
value = deserialized_data[prop.name]
|
|
507
|
+
if value is not None:
|
|
508
|
+
setattr(self, prop.name, value)
|
|
509
|
+
assert self.remote_data is not None
|
|
510
|
+
for key, value in data.items():
|
|
511
|
+
if not (key.startswith("Q") or key == "@context"):
|
|
512
|
+
expanded_path = expand_uri(key, cls.context)
|
|
513
|
+
assert isinstance(expanded_path, str)
|
|
514
|
+
self.remote_data[expanded_path] = data[key]
|
|
515
|
+
if self.space is None and "https://core.kg.ebrains.eu/vocab/meta/space" in data:
|
|
516
|
+
self._space = data["https://core.kg.ebrains.eu/vocab/meta/space"]
|
|
517
|
+
|
|
518
|
+
def __eq__(self, other):
|
|
519
|
+
return not self.__ne__(other)
|
|
520
|
+
|
|
521
|
+
def __ne__(self, other):
|
|
522
|
+
if not isinstance(other, self.__class__):
|
|
523
|
+
return True
|
|
524
|
+
if self.id and other.id and self.id != other.id:
|
|
525
|
+
return True
|
|
526
|
+
for prop in self.properties:
|
|
527
|
+
val_self = getattr(self, prop.name)
|
|
528
|
+
val_other = getattr(other, prop.name)
|
|
529
|
+
if val_self != val_other:
|
|
530
|
+
return True
|
|
531
|
+
return False
|
|
532
|
+
|
|
533
|
+
def diff(self, other):
|
|
534
|
+
"""
|
|
535
|
+
Return a dictionary containing the differences between two metadata objects.
|
|
536
|
+
"""
|
|
537
|
+
differences = defaultdict(dict)
|
|
538
|
+
if not isinstance(other, self.__class__):
|
|
539
|
+
differences["type"] = (self.__class__, other.__class__)
|
|
540
|
+
else:
|
|
541
|
+
if self.id != other.id:
|
|
542
|
+
differences["id"] = (self.id, other.id)
|
|
543
|
+
for prop in self.properties:
|
|
544
|
+
val_self = getattr(self, prop.name)
|
|
545
|
+
val_other = getattr(other, prop.name)
|
|
546
|
+
if val_self != val_other:
|
|
547
|
+
differences["properties"][prop.name] = (val_self, val_other)
|
|
548
|
+
return differences
|
|
549
|
+
|
|
550
|
+
def exists(self, client: KGClient, ignore_duplicates: bool = False, in_spaces: Optional[List[str]] = None) -> bool:
|
|
551
|
+
"""Check if this object already exists in the KnowledgeGraph"""
|
|
552
|
+
|
|
553
|
+
if self.id and self.id.startswith("http"):
|
|
554
|
+
# Since the KG now allows user-specified IDs we can't assume that the presence of
|
|
555
|
+
# an id means the object exists
|
|
556
|
+
data = client.instance_from_full_uri(
|
|
557
|
+
self.id, use_cache=True, release_status=self.release_status or "any", require_full_data=False
|
|
558
|
+
)
|
|
559
|
+
if self._raw_remote_data is None:
|
|
560
|
+
self._raw_remote_data = data
|
|
561
|
+
obj_exists = bool(data)
|
|
562
|
+
if obj_exists:
|
|
563
|
+
self._update_empty_properties(data) # also updates `remote_data`
|
|
564
|
+
return obj_exists
|
|
565
|
+
else:
|
|
566
|
+
try:
|
|
567
|
+
query_filter = self._build_existence_query()
|
|
568
|
+
except CannotBuildExistenceQuery:
|
|
569
|
+
return False
|
|
570
|
+
|
|
571
|
+
if query_filter is None:
|
|
572
|
+
# if there's no existence query and no ID, we allow
|
|
573
|
+
# duplicate entries
|
|
574
|
+
return False
|
|
575
|
+
else:
|
|
576
|
+
query_cache_key = generate_cache_key(query_filter)
|
|
577
|
+
if query_cache_key in save_cache[self.__class__]:
|
|
578
|
+
# Because the KnowledgeGraph is only eventually consistent, an instance
|
|
579
|
+
# that has just been written to the KG may not appear in the query.
|
|
580
|
+
# Therefore we cache the query when creating an instance and
|
|
581
|
+
# where exists() returns True
|
|
582
|
+
self.id = save_cache[self.__class__][query_cache_key]
|
|
583
|
+
cached_obj = object_cache.get(self.id)
|
|
584
|
+
if cached_obj and cached_obj.remote_data:
|
|
585
|
+
self._raw_remote_data = cached_obj._raw_remote_data
|
|
586
|
+
self.remote_data = cached_obj.remote_data # copy or update needed?
|
|
587
|
+
return True
|
|
588
|
+
|
|
589
|
+
query = self.__class__.generate_minimal_query(
|
|
590
|
+
client=client,
|
|
591
|
+
filters=query_filter,
|
|
592
|
+
)
|
|
593
|
+
|
|
594
|
+
try:
|
|
595
|
+
instances = client.query(query=query, size=2, release_status="any", restrict_to_spaces=in_spaces).data
|
|
596
|
+
except ConnectionError as err:
|
|
597
|
+
if "RemoteDisconnected" in str(err):
|
|
598
|
+
warn(
|
|
599
|
+
f"Timeout when checking for existence of object {self}."
|
|
600
|
+
"Returning False, check for possible creation of duplicate instances."
|
|
601
|
+
)
|
|
602
|
+
return False
|
|
603
|
+
|
|
604
|
+
if instances:
|
|
605
|
+
if len(instances) > 1 and not ignore_duplicates:
|
|
606
|
+
# we might want to consider running a second query with "equals" rather than "contains"
|
|
607
|
+
raise Exception(
|
|
608
|
+
f"Existence query is not specific enough. Type: {self.__class__.__name__}; filters: {query_filter}"
|
|
609
|
+
)
|
|
610
|
+
|
|
611
|
+
# it seems that sometimes the "query" endpoint returns instances
|
|
612
|
+
# which the "instances" endpoint doesn't know about, so here we double check that
|
|
613
|
+
# the instance can be found
|
|
614
|
+
instance = client.instance_from_full_uri(instances[0]["@id"], release_status="any")
|
|
615
|
+
if instance is None:
|
|
616
|
+
return False
|
|
617
|
+
|
|
618
|
+
self.id = instance["@id"]
|
|
619
|
+
assert isinstance(self.id, str)
|
|
620
|
+
save_cache[self.__class__][query_cache_key] = self.id
|
|
621
|
+
self._update_empty_properties(instance) # also updates `remote_data`
|
|
622
|
+
return bool(instances)
|
|
623
|
+
|
|
624
|
+
def modified_data(self) -> JSONdict:
|
|
625
|
+
"""
|
|
626
|
+
Return a dict containing the properties that have been modified locally
|
|
627
|
+
from the values originally obtained from the Knowledge Graph.
|
|
628
|
+
"""
|
|
629
|
+
|
|
630
|
+
def values_are_equal(local, remote):
|
|
631
|
+
if type(local) != type(remote):
|
|
632
|
+
return False
|
|
633
|
+
if isinstance(local, list):
|
|
634
|
+
if len(local) != len(remote):
|
|
635
|
+
return False
|
|
636
|
+
return all(values_are_equal(a, b) for a, b in zip(local, remote))
|
|
637
|
+
elif isinstance(local, dict):
|
|
638
|
+
return all(
|
|
639
|
+
values_are_equal(local[key], remote.get(key, None))
|
|
640
|
+
for key in local.keys()
|
|
641
|
+
if not (local[key] is None and key not in remote)
|
|
642
|
+
)
|
|
643
|
+
else:
|
|
644
|
+
return local == remote
|
|
645
|
+
|
|
646
|
+
current_data = normalize_data(
|
|
647
|
+
self.to_jsonld(include_empty_properties=True, embed_linked_nodes=False), self.context
|
|
648
|
+
)
|
|
649
|
+
modified_data = {}
|
|
650
|
+
for key, current_value in current_data.items():
|
|
651
|
+
if not key.startswith("@"):
|
|
652
|
+
assert key.startswith("http") # keys should all be expanded by this point
|
|
653
|
+
assert self.remote_data is not None
|
|
654
|
+
remote_value = self.remote_data.get(key, None)
|
|
655
|
+
if not values_are_equal(current_value, remote_value):
|
|
656
|
+
modified_data[key] = current_value
|
|
657
|
+
return modified_data
|
|
658
|
+
|
|
659
|
+
def save(
|
|
660
|
+
self,
|
|
661
|
+
client: KGClient,
|
|
662
|
+
space: Optional[str] = None,
|
|
663
|
+
recursive: bool = True,
|
|
664
|
+
activity_log: Optional[ActivityLog] = None,
|
|
665
|
+
replace: bool = False,
|
|
666
|
+
ignore_auth_errors: bool = False,
|
|
667
|
+
ignore_duplicates: bool = False,
|
|
668
|
+
):
|
|
669
|
+
"""
|
|
670
|
+
Store the current object in the Knowledge Graph, either updating an existing instance
|
|
671
|
+
or creating a new one as appropriate.
|
|
672
|
+
|
|
673
|
+
Args:
|
|
674
|
+
client: KGClient object that handles the communication with the KG.
|
|
675
|
+
space (str, optional): The KG space to save the object in. If not provided, a default space is used depending on the object type.
|
|
676
|
+
recursive (bool, optional): Whether to recursively save any children of this object. Defaults to True.
|
|
677
|
+
activity_log (ActivityLog, optional): An `ActivityLog` instance to log the operations performed during the save operation.
|
|
678
|
+
This is particularly helpful with `recursive=True`.
|
|
679
|
+
replace (bool, optional): Whether to completely replace an existing KG instance with this one, or just update the existing object
|
|
680
|
+
with any modified properties. Defaults to False.
|
|
681
|
+
ignore_auth_errors (bool, optional): Whether to continue silently when encountering authentication errors. Defaults to False.
|
|
682
|
+
ignore_duplicates (bool, optional): Whether to ignore the existence of multiple objects with the same properties
|
|
683
|
+
(and consider only the first in the list), or to raise an Exception. Defaults to False.
|
|
684
|
+
|
|
685
|
+
Raises:
|
|
686
|
+
- An `AuthorizationError` if the current user is not authorized to perform the requested operation.
|
|
687
|
+
|
|
688
|
+
"""
|
|
689
|
+
if recursive:
|
|
690
|
+
for prop in self.properties:
|
|
691
|
+
# We do not save reverse properties, those objects must be saved separately.
|
|
692
|
+
# This could be revisited, but we'll have to be careful about loops
|
|
693
|
+
# if saving recursively
|
|
694
|
+
values = getattr(self, prop.name)
|
|
695
|
+
for value in as_list(values):
|
|
696
|
+
if isinstance(value, ContainsMetadata):
|
|
697
|
+
target_space: Optional[str]
|
|
698
|
+
if (
|
|
699
|
+
isinstance(value, KGObject)
|
|
700
|
+
and value.__class__.default_space == "controlled"
|
|
701
|
+
and value.exists(client, ignore_duplicates=ignore_duplicates)
|
|
702
|
+
and value.space == "controlled"
|
|
703
|
+
):
|
|
704
|
+
continue
|
|
705
|
+
elif value.space:
|
|
706
|
+
target_space = value.space
|
|
707
|
+
elif space is None and self.space is not None:
|
|
708
|
+
target_space = self.space
|
|
709
|
+
else:
|
|
710
|
+
target_space = space
|
|
711
|
+
if target_space == "controlled":
|
|
712
|
+
assert isinstance(value, KGObject) # for type checking
|
|
713
|
+
if (
|
|
714
|
+
value.exists(client, ignore_duplicates=ignore_duplicates)
|
|
715
|
+
and value.space == "controlled"
|
|
716
|
+
):
|
|
717
|
+
continue
|
|
718
|
+
else:
|
|
719
|
+
raise AuthorizationError("Cannot write to controlled space")
|
|
720
|
+
value.save(
|
|
721
|
+
client,
|
|
722
|
+
space=target_space,
|
|
723
|
+
recursive=True,
|
|
724
|
+
activity_log=activity_log,
|
|
725
|
+
ignore_duplicates=ignore_duplicates,
|
|
726
|
+
)
|
|
727
|
+
if space is None:
|
|
728
|
+
if self.space is None:
|
|
729
|
+
space = self.__class__.default_space
|
|
730
|
+
else:
|
|
731
|
+
space = self.space
|
|
732
|
+
logger.info(f"Saving a {self.__class__.__name__} in space {space}")
|
|
733
|
+
if self.exists(client, ignore_duplicates=ignore_duplicates, in_spaces=[space]):
|
|
734
|
+
if not self.allow_update:
|
|
735
|
+
logger.info(f" - not updating {self.__class__.__name__}(id={self.id}), update not allowed by user")
|
|
736
|
+
if activity_log:
|
|
737
|
+
activity_log.update(item=self, delta=None, space=space, entry_type="no-op")
|
|
738
|
+
else:
|
|
739
|
+
# update
|
|
740
|
+
local_data = normalize_data(
|
|
741
|
+
self.to_jsonld(include_empty_properties=False, embed_linked_nodes=False),
|
|
742
|
+
self.context
|
|
743
|
+
)
|
|
744
|
+
if replace:
|
|
745
|
+
logger.info(f" - replacing - {self.__class__.__name__}(id={self.id})")
|
|
746
|
+
if activity_log:
|
|
747
|
+
activity_log.update(item=self, delta=local_data, space=space, entry_type="replacement")
|
|
748
|
+
try:
|
|
749
|
+
client.replace_instance(self.uuid, local_data)
|
|
750
|
+
# what does this return? Can we use it to update `remote_data`?
|
|
751
|
+
except AuthorizationError as err:
|
|
752
|
+
if ignore_auth_errors:
|
|
753
|
+
logger.error(str(err))
|
|
754
|
+
else:
|
|
755
|
+
raise
|
|
756
|
+
else:
|
|
757
|
+
self.remote_data = local_data
|
|
758
|
+
else:
|
|
759
|
+
modified_data = self.modified_data()
|
|
760
|
+
if modified_data:
|
|
761
|
+
logger.info(
|
|
762
|
+
f" - updating - {self.__class__.__name__}(id={self.id}) - properties changed: {modified_data.keys()}"
|
|
763
|
+
)
|
|
764
|
+
skip_update = False
|
|
765
|
+
if "storageSize" in modified_data:
|
|
766
|
+
warn("Removing storage size from update because this prop is currently locked by the KG")
|
|
767
|
+
modified_data.pop("storageSize")
|
|
768
|
+
skip_update = len(modified_data) == 0
|
|
769
|
+
|
|
770
|
+
if skip_update:
|
|
771
|
+
if activity_log:
|
|
772
|
+
activity_log.update(item=self, delta=None, space=space, entry_type="no-op")
|
|
773
|
+
else:
|
|
774
|
+
try:
|
|
775
|
+
# Note: if modified_data includes embedded objects
|
|
776
|
+
# then _all_ fields of the embedded objects must be provided,
|
|
777
|
+
# not only those that have changed.
|
|
778
|
+
client.update_instance(self.uuid, modified_data)
|
|
779
|
+
except AuthorizationError as err:
|
|
780
|
+
if ignore_auth_errors:
|
|
781
|
+
logger.error(str(err))
|
|
782
|
+
else:
|
|
783
|
+
raise
|
|
784
|
+
else:
|
|
785
|
+
self.remote_data = local_data
|
|
786
|
+
if activity_log:
|
|
787
|
+
activity_log.update(
|
|
788
|
+
item=self,
|
|
789
|
+
delta=modified_data,
|
|
790
|
+
space=space,
|
|
791
|
+
entry_type="update",
|
|
792
|
+
)
|
|
793
|
+
else:
|
|
794
|
+
logger.info(f" - not updating {self.__class__.__name__}(id={self.id}), unchanged")
|
|
795
|
+
if activity_log:
|
|
796
|
+
activity_log.update(item=self, delta=None, space=space, entry_type="no-op")
|
|
797
|
+
else:
|
|
798
|
+
# create new
|
|
799
|
+
local_data = normalize_data(
|
|
800
|
+
self.to_jsonld(include_empty_properties=False, embed_linked_nodes=False),
|
|
801
|
+
self.context
|
|
802
|
+
)
|
|
803
|
+
logger.info(" - creating instance with data {}".format(local_data))
|
|
804
|
+
if self.id and self.id.startswith("http"):
|
|
805
|
+
instance_id = self.uuid
|
|
806
|
+
else:
|
|
807
|
+
instance_id = None
|
|
808
|
+
try:
|
|
809
|
+
instance_data = client.create_new_instance(
|
|
810
|
+
local_data, space or self.__class__.default_space, instance_id=instance_id
|
|
811
|
+
)
|
|
812
|
+
except (AuthorizationError, ResourceExistsError) as err:
|
|
813
|
+
if ignore_auth_errors:
|
|
814
|
+
logger.error(str(err))
|
|
815
|
+
if activity_log:
|
|
816
|
+
activity_log.update(
|
|
817
|
+
item=self,
|
|
818
|
+
delta=local_data,
|
|
819
|
+
space=self.space,
|
|
820
|
+
entry_type="create-error",
|
|
821
|
+
)
|
|
822
|
+
else:
|
|
823
|
+
raise
|
|
824
|
+
else:
|
|
825
|
+
self.id = instance_data["@id"]
|
|
826
|
+
self._raw_remote_data = instance_data
|
|
827
|
+
self.remote_data = local_data
|
|
828
|
+
if activity_log:
|
|
829
|
+
activity_log.update(item=self, delta=instance_data, space=self.space, entry_type="create")
|
|
830
|
+
|
|
831
|
+
# not handled yet: if an existing object is in a different space to the one specified here,
|
|
832
|
+
# should we move it to the new space, or raise an Exception?
|
|
833
|
+
if self.id:
|
|
834
|
+
logger.debug(
|
|
835
|
+
"Updating cache for object {}. Current state: {}".format(
|
|
836
|
+
self.id, self.to_jsonld(embed_linked_nodes=False)
|
|
837
|
+
)
|
|
838
|
+
)
|
|
839
|
+
object_cache[self.id] = self
|
|
840
|
+
else:
|
|
841
|
+
logger.warning("Object has no id - see log for the underlying error")
|
|
842
|
+
return self.id
|
|
843
|
+
|
|
844
|
+
def delete(self, client: KGClient, ignore_not_found: bool = True):
|
|
845
|
+
"""Delete the current metadata object from the KG.
|
|
846
|
+
|
|
847
|
+
If `ignore_not_found` is False, an exception will be raised if the object does
|
|
848
|
+
not exist. Otherwise, the method will finish silently.
|
|
849
|
+
"""
|
|
850
|
+
client.delete_instance(self.uuid, ignore_not_found=ignore_not_found)
|
|
851
|
+
if self.id in object_cache:
|
|
852
|
+
object_cache.pop(self.id)
|
|
853
|
+
|
|
854
|
+
def dump(self, file_path, indent=2):
|
|
855
|
+
"""
|
|
856
|
+
Save this object to a file in JSON-LD format.
|
|
857
|
+
"""
|
|
858
|
+
LinkedMetadata.save(self, file_path, indent)
|
|
859
|
+
|
|
860
|
+
@classmethod
|
|
861
|
+
def by_name(
|
|
862
|
+
cls,
|
|
863
|
+
name: str,
|
|
864
|
+
client: Optional[KGClient] = None,
|
|
865
|
+
match: str = "equals",
|
|
866
|
+
all: bool = False,
|
|
867
|
+
space: Optional[str] = None,
|
|
868
|
+
release_status: str = "released",
|
|
869
|
+
scope: Optional[str] = None,
|
|
870
|
+
follow_links: Optional[Dict[str, Any]] = None,
|
|
871
|
+
) -> Union[KGObject, List[KGObject], None]:
|
|
872
|
+
"""
|
|
873
|
+
Retrieve an instance from the Knowledge Graph based on its name.
|
|
874
|
+
|
|
875
|
+
This includes properties "name", "lookup_label", "family_name", "full_name", "short_name", "abbreviation", and "synonyms".
|
|
876
|
+
|
|
877
|
+
Note that not all metadata classes have a name.
|
|
878
|
+
|
|
879
|
+
Args:
|
|
880
|
+
name (str): a string to search for.
|
|
881
|
+
client: a KGClient
|
|
882
|
+
match (str, optional): either "equals" (exact match - default) or "contains".
|
|
883
|
+
all (bool, optional): Whether to return all objects that match the name, or only the first. Defaults to False.
|
|
884
|
+
space (str, optional): the KG space to search in. Default is to search in all available spaces.
|
|
885
|
+
release_status (str, optional): The scope of the search. Valid values are "released", "in progress", or "any".
|
|
886
|
+
Defaults to "released".
|
|
887
|
+
follow_links (dict): The links in the graph to follow. Defaults to None.
|
|
888
|
+
|
|
889
|
+
"""
|
|
890
|
+
release_status = handle_scope_keyword(scope, release_status)
|
|
891
|
+
# todo: move this to openminds generation, and include only in those subclasses
|
|
892
|
+
# that have a name-like property
|
|
893
|
+
namelike_properties = ("name", "lookup_label", "family_name", "full_name", "short_name", "abbreviation", "synonyms")
|
|
894
|
+
objects = []
|
|
895
|
+
if client:
|
|
896
|
+
kwargs = dict(space=space, release_status=release_status, api="query", follow_links=follow_links)
|
|
897
|
+
for prop_name in namelike_properties:
|
|
898
|
+
if prop_name in cls.property_names:
|
|
899
|
+
kwargs[prop_name] = name
|
|
900
|
+
break
|
|
901
|
+
objects = cls.list(client, **kwargs)
|
|
902
|
+
if match == "equals":
|
|
903
|
+
objects = [
|
|
904
|
+
obj for obj in objects
|
|
905
|
+
if any(
|
|
906
|
+
getattr(obj, prop_name, None) == name
|
|
907
|
+
for prop_name in namelike_properties
|
|
908
|
+
)
|
|
909
|
+
]
|
|
910
|
+
elif hasattr(cls, "instances"): # controlled terms, etc.
|
|
911
|
+
if cls._instance_lookup is None:
|
|
912
|
+
cls._instance_lookup = {}
|
|
913
|
+
for instance in cls.instances():
|
|
914
|
+
keys = []
|
|
915
|
+
for prop_name in namelike_properties[:-1]: # handle 'synonyms' separately
|
|
916
|
+
if hasattr(instance, prop_name):
|
|
917
|
+
keys.append(getattr(instance, prop_name))
|
|
918
|
+
if hasattr(instance, "synonyms"):
|
|
919
|
+
for synonym in instance.synonyms or []:
|
|
920
|
+
keys.append(synonym)
|
|
921
|
+
for key in keys:
|
|
922
|
+
if key in cls._instance_lookup:
|
|
923
|
+
cls._instance_lookup[key].append(instance)
|
|
924
|
+
else:
|
|
925
|
+
cls._instance_lookup[key] = [instance]
|
|
926
|
+
if match == "equals":
|
|
927
|
+
objects = cls._instance_lookup.get(name, None)
|
|
928
|
+
elif match == "contains":
|
|
929
|
+
objects = []
|
|
930
|
+
for key, instances in cls._instance_lookup.items():
|
|
931
|
+
if name in key:
|
|
932
|
+
objects.extend(instances)
|
|
933
|
+
else:
|
|
934
|
+
raise ValueError("'match' must be either 'exact' or 'contains'")
|
|
935
|
+
if len(objects) == 0:
|
|
936
|
+
return None
|
|
937
|
+
elif all:
|
|
938
|
+
return objects
|
|
939
|
+
elif len(objects) == 1:
|
|
940
|
+
return objects[0]
|
|
941
|
+
else:
|
|
942
|
+
warn("Multiple objects with the same name, returning the first. " "Use 'all=True' to retrieve them all")
|
|
943
|
+
return objects[0]
|
|
944
|
+
|
|
945
|
+
def show(self, max_width: Optional[int] = 120, include_empty_properties=False):
|
|
946
|
+
"""
|
|
947
|
+
Print a table showing the metadata contained in this object.
|
|
948
|
+
"""
|
|
949
|
+
if not have_tabulate:
|
|
950
|
+
raise Exception("You need to install the tabulate module to use the `show()` method")
|
|
951
|
+
data = [
|
|
952
|
+
("id", str(self.id)),
|
|
953
|
+
("space", str(self.space)),
|
|
954
|
+
("type", self.type_),
|
|
955
|
+
]
|
|
956
|
+
for prop in self.__class__.all_properties:
|
|
957
|
+
value = getattr(self, prop.name, None)
|
|
958
|
+
if include_empty_properties or not isinstance(value, (type(None), KGQuery)):
|
|
959
|
+
data.append((prop.name, str(value)))
|
|
960
|
+
if max_width:
|
|
961
|
+
value_column_width = max_width - max(len(item[0]) for item in data)
|
|
962
|
+
|
|
963
|
+
def fit_column(value):
|
|
964
|
+
strv = value
|
|
965
|
+
if len(strv) > value_column_width:
|
|
966
|
+
strv = strv[: value_column_width - 4] + " ..."
|
|
967
|
+
return strv
|
|
968
|
+
|
|
969
|
+
data = [(k, fit_column(v)) for k, v in data]
|
|
970
|
+
print(tabulate(data, tablefmt="plain"))
|
|
971
|
+
# return tabulate(data, tablefmt='html') - also see https://bitbucket.org/astanin/python-tabulate/issues/57/html-class-options-for-tables
|
|
972
|
+
|
|
973
|
+
@classmethod
|
|
974
|
+
def generate_query(
|
|
975
|
+
cls,
|
|
976
|
+
client: KGClient,
|
|
977
|
+
space: Union[str, None],
|
|
978
|
+
filters: Optional[Dict[str, Any]] = None,
|
|
979
|
+
follow_links: Optional[Dict[str, Any]] = None,
|
|
980
|
+
with_reverse_properties: Optional[bool] = False,
|
|
981
|
+
label: Optional[str] = None,
|
|
982
|
+
) -> Union[Dict[str, Any], None]:
|
|
983
|
+
"""
|
|
984
|
+
Generate a KG query definition as a JSON-LD document.
|
|
985
|
+
|
|
986
|
+
Args:
|
|
987
|
+
client: KGClient object that handles the communication with the KG.
|
|
988
|
+
space (str, optional): if provided, restrict the query to metadata stored in the given KG space.
|
|
989
|
+
filters (dict): A dictonary defining search parameters for the query.
|
|
990
|
+
follow_links (dict): The links in the graph to follow. Defaults to None.
|
|
991
|
+
with_reverse_properties (dict): Whether to include reverse properties. Default False.
|
|
992
|
+
label (str, optional): a label for the query
|
|
993
|
+
|
|
994
|
+
Returns:
|
|
995
|
+
A JSON-LD document containing the KG query definition.
|
|
996
|
+
|
|
997
|
+
"""
|
|
998
|
+
if space == "myspace":
|
|
999
|
+
real_space = client._private_space
|
|
1000
|
+
else:
|
|
1001
|
+
real_space = space
|
|
1002
|
+
|
|
1003
|
+
if filters:
|
|
1004
|
+
normalized_filters = cls.normalize_filter(expand_filter(filters))
|
|
1005
|
+
else:
|
|
1006
|
+
normalized_filters = None
|
|
1007
|
+
# first pass, we build the basic structure
|
|
1008
|
+
query = Query(
|
|
1009
|
+
node_type=cls.type_,
|
|
1010
|
+
label=label,
|
|
1011
|
+
space=real_space,
|
|
1012
|
+
properties=cls.generate_query_properties(follow_links, with_reverse_properties),
|
|
1013
|
+
)
|
|
1014
|
+
# second pass, we add filters
|
|
1015
|
+
query.properties.extend(cls.generate_query_filter_properties(normalized_filters))
|
|
1016
|
+
# third pass, we add sorting, which can only happen at the top level
|
|
1017
|
+
for prop in query.properties:
|
|
1018
|
+
if prop.name in ("name", "fullName", "lookupLabel"):
|
|
1019
|
+
prop.sorted = True
|
|
1020
|
+
# implementation note: the three-pass approach generates queries that are sometimes more verbose
|
|
1021
|
+
# than necessary, but it makes the logic easier to understand.
|
|
1022
|
+
return query.serialize()
|
|
1023
|
+
|
|
1024
|
+
@classmethod
|
|
1025
|
+
def generate_minimal_query(
|
|
1026
|
+
cls,
|
|
1027
|
+
client: KGClient,
|
|
1028
|
+
filters: Optional[Dict[str, Any]] = None,
|
|
1029
|
+
label: Optional[str] = None,
|
|
1030
|
+
) -> Union[Dict[str, Any], None]:
|
|
1031
|
+
"""
|
|
1032
|
+
Generate a minimal KG query definition as a JSON-LD document.
|
|
1033
|
+
Such a query returns only the @id of any instances that are found.
|
|
1034
|
+
|
|
1035
|
+
Args:
|
|
1036
|
+
client: KGClient object that handles the communication with the KG.
|
|
1037
|
+
filters (dict): A dictonary defining search parameters for the query.
|
|
1038
|
+
label (str, optional): a label for the query
|
|
1039
|
+
|
|
1040
|
+
Returns:
|
|
1041
|
+
A JSON-LD document containing the KG query definition.
|
|
1042
|
+
|
|
1043
|
+
"""
|
|
1044
|
+
if filters:
|
|
1045
|
+
normalized_filters = cls.normalize_filter(expand_filter(filters))
|
|
1046
|
+
else:
|
|
1047
|
+
normalized_filters = None
|
|
1048
|
+
# first pass, we build the basic structure
|
|
1049
|
+
query = Query(
|
|
1050
|
+
node_type=cls.type_,
|
|
1051
|
+
label=label,
|
|
1052
|
+
space=None,
|
|
1053
|
+
properties=[QueryProperty("@type")],
|
|
1054
|
+
)
|
|
1055
|
+
# second pass, we add filters
|
|
1056
|
+
query.properties.extend(cls.generate_query_filter_properties(normalized_filters))
|
|
1057
|
+
return query.serialize()
|
|
1058
|
+
|
|
1059
|
+
def children(
|
|
1060
|
+
self, client: KGClient, follow_links: Optional[Dict[str, Any]] = None
|
|
1061
|
+
) -> List[RepresentsSingleObject]:
|
|
1062
|
+
"""Return a list of child objects."""
|
|
1063
|
+
if follow_links:
|
|
1064
|
+
self.resolve(client, follow_links=follow_links)
|
|
1065
|
+
all_children = []
|
|
1066
|
+
for prop in self.properties:
|
|
1067
|
+
if prop.is_link:
|
|
1068
|
+
children = as_list(getattr(self, prop.name))
|
|
1069
|
+
all_children.extend(children)
|
|
1070
|
+
if follow_links:
|
|
1071
|
+
for child in children:
|
|
1072
|
+
all_children.extend(child.children(client))
|
|
1073
|
+
return all_children
|
|
1074
|
+
|
|
1075
|
+
def export(self, path: str, single_file: bool = False):
|
|
1076
|
+
"""
|
|
1077
|
+
Export metadata as files in JSON-LD format.
|
|
1078
|
+
|
|
1079
|
+
If any objects do not have IDs, these will be generated.
|
|
1080
|
+
|
|
1081
|
+
If `single_file` is False, then `path` must be the path to a directory,
|
|
1082
|
+
and each object will be exported as a file named for the object ID.
|
|
1083
|
+
|
|
1084
|
+
If `single_file` is True, then `path` should be the path to a file
|
|
1085
|
+
with extension ".jsonld". This file will contain metadata for all objects.
|
|
1086
|
+
"""
|
|
1087
|
+
raise NotImplementedError("todo")
|