Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
3 changes: 3 additions & 0 deletions builder/fairgraph_module_template.py.txt
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ class {{ class_name }}({{base_class}}, OM{{ class_name }}):
aliases = {{ aliases }}
{%- endif %}
existence_query_properties = {{ existence_query_properties }}
{%- if sort_property %}
sort_property = "{{ sort_property }}"
{%- endif %}

def __init__(self {%- for arg in constructor_arguments -%}, {{arg}}=None{%- endfor -%}, id=None, data=None, space=None, release_status=None):
return {{ base_class }}.__init__(self, {{ standard_init_properties }}data=data {%- for arg in constructor_arguments -%}, {{arg}}={{arg}}{%- endfor -%})
Expand Down
28 changes: 28 additions & 0 deletions builder/update_openminds.py
Original file line number Diff line number Diff line change
Expand Up @@ -697,6 +697,33 @@ def get_existence_query(cls_name, properties):
return tuple(required_property_names)


# The KG query API allows results to be sorted by only one property, at the root level.
# By default we sort by the first of these properties that a class has.
# "short_name" and "abbreviation" are not included since, in openMINDS, they never appear
# without "full_name" or "name"; "synonyms" is not included since it is list-valued.
SORT_PROPERTY_PRIORITY = ("name", "full_name", "lookup_label", "family_name")

# For some classes the default sort property is not the most useful one.
custom_sort_properties = {
# the lookup label is prefixed by the parcellation (and version), so sorting by it keeps
# entities from the same atlas together, whereas sorting by name would interleave them
"ParcellationEntity": "lookup_label",
"ParcellationEntityVersion": "lookup_label",
}


def get_sort_property(cls_name, properties):
property_names = [prop["name"] for prop in properties]
if cls_name in custom_sort_properties:
sort_property = custom_sort_properties[cls_name]
assert sort_property in property_names, f"{cls_name} has no property '{sort_property}'"
return sort_property
for property_name in SORT_PROPERTY_PRIORITY:
if property_name in property_names:
return property_name
return None


def property_name_sort_key(property_name):
"""Sort the name prop to be first"""
priorities = {
Expand Down Expand Up @@ -948,6 +975,7 @@ def get_type(prop):
"properties": sorted(properties, key=lambda p: p["name"]),
"reverse_properties": sorted(reverse_properties, key=lambda p: p["name"]),
"existence_query_properties": get_existence_query(class_name, properties),
"sort_property": get_sort_property(class_name, properties) if base_class == "KGObject" else None,
"standard_init_properties": standard_init_properties,
"additional_methods": additional_methods,
"aliases": aliases,
Expand Down
4 changes: 4 additions & 0 deletions doc/contributing.rst
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,10 @@ Reverse properties (links pointing *into* a class) get their names from the
schema introduces a property that has no entry there, generation fails with a :exc:`KeyError`
naming the class, and an entry needs to be added.

Each class's ``sort_property``, the property by which query results are sorted, is chosen by
``get_sort_property()`` in :file:`builder/update_openminds.py`: the first property in
``SORT_PROPERTY_PRIORITY`` that the class has, unless the class has an entry in ``custom_sort_properties``.

.. warning::

Everything under :file:`fairgraph/openminds/v4` and :file:`fairgraph/openminds/v5` is
Expand Down
26 changes: 26 additions & 0 deletions doc/queries.rst
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,32 @@ This returns 10 nodes starting with the 15th. To see how many nodes there are in
.. note:: if you consistently retrieve an empty list, it is probably because you do not
yet have the necessary permissions. See :doc:`permissions` for more information.

Ordering of results
-------------------

When results are retrieved using the KG query API (``api="query"``), they are sorted in ascending order,
ignoring case, by a single property of the class, given by its ``sort_property`` attribute::

>>> Person.sort_property
'family_name'

Nodes that lack a value for this property come first.
The KG allows sorting by only one property, so fairgraph chooses the first of
``name``, ``full_name``, ``lookup_label`` and ``family_name`` that a class has.
The exceptions are :class:`ParcellationEntity` and :class:`ParcellationEntityVersion`, which are sorted by
``lookup_label``: this is prefixed by the parcellation (and version), so entities from the same atlas stay together.
Other name-like properties are not used for sorting: ``short_name`` and ``abbreviation`` are always accompanied
by ``full_name`` or ``name``, and ``synonyms`` has multiple values.
Classes without any of these properties (``sort_property`` is ``None``) are returned in the order the KG supplies.

When :meth:`list()` is called without filters and without ``follow_links``, it uses the KG core API by default,
and this does not support sorting. For sorted results in this case, pass ``api="query"``::

licenses = License.list(client, api="query")

(The ``ensure_order`` option of :class:`~fairgraph.queries.QueryProperty` is unrelated to this:
it preserves the order of the values of a property with multiple values, rather than sorting the results.)


Filtering/searching
===================
Expand Down
8 changes: 8 additions & 0 deletions doc/release_notes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,14 @@ Changes in behaviour
(the default is ``existence_match="equals"``). With ``recursive=True`` the setting applies to
child objects as well.

- Queries generated by fairgraph now sort results by exactly one property, as the KG query API requires.
Previously, classes with both ``name`` and ``lookup_label`` requested sorting by both, and the KG used
whichever came first. The property is now given by each class's new ``sort_property`` attribute.
As a result, :class:`SlicingDevice`, :class:`Electrode`, :class:`ElectrodeArray` and :class:`Pipette`
are now sorted by ``name`` instead of ``lookup_label``, and :class:`Person`, previously unsorted,
is now sorted by ``family_name``. The ordering of results is now described in :doc:`queries`
(`#129 <https://github.com/HumanBrainProject/fairgraph/issues/129>`_).

Bug fixes
---------

Expand Down
18 changes: 14 additions & 4 deletions fairgraph/kgobject.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,9 @@ class KGObject(KGNode, Releasable):
# many cases be over-ridden.
# It assumes that "name" is unique within instances of a given type,
# which may often not be the case.
sort_property: Optional[str] = None
# The property by which results of KG queries are sorted, if any.
# Set in the generated subclasses; see builder/update_openminds.py

def __init__(
self,
Expand Down Expand Up @@ -394,6 +397,10 @@ def list(

Returns:
A list of instances of this class representing the objects returned by the KG query.
With the query API, results are sorted in ascending order, ignoring case, by the property
named in the class's `sort_property` attribute, if it has one.
The core API does not sort results, and is used by default when there are no filters and
no `follow_links`: pass `api="query"` for sorted results.

Raises:
ValueError: If invalid arguments are passed to the method.
Expand Down Expand Up @@ -1182,10 +1189,13 @@ def generate_query(
)
# second pass, we add filters
query.properties.extend(cls.generate_query_filter_properties(normalized_filters))
# third pass, we add sorting, which can only happen at the top level
for prop in query.properties:
if prop.name in ("name", "fullName", "lookupLabel"):
prop.sorted = True
# third pass, we add sorting, which the KG allows on only one property, at the top level
if cls.sort_property:
sort_path = cls._property_lookup[cls.sort_property].path
for prop in query.properties:
if prop.name == sort_path:
prop.sorted = True
break
# implementation note: the three-pass approach generates queries that are sometimes more verbose
# than necessary, but it makes the logic easier to understand.
return query.serialize()
Expand Down
1 change: 1 addition & 0 deletions fairgraph/openminds/v4/chemicals/chemical_mixture.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ class ChemicalMixture(KGObject, OMChemicalMixture):
),
]
existence_query_properties = ("has_parts", "type")
sort_property = "name"

def __init__(
self,
Expand Down
1 change: 1 addition & 0 deletions fairgraph/openminds/v4/chemicals/chemical_substance.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ class ChemicalSubstance(KGObject, OMChemicalSubstance):
),
]
existence_query_properties = ("lookup_label",)
sort_property = "lookup_label"

def __init__(
self,
Expand Down
1 change: 1 addition & 0 deletions fairgraph/openminds/v4/computation/data_analysis.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ class DataAnalysis(KGObject, OMDataAnalysis):
),
]
existence_query_properties = ("lookup_label",)
sort_property = "lookup_label"

def __init__(
self,
Expand Down
1 change: 1 addition & 0 deletions fairgraph/openminds/v4/computation/data_copy.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ class DataCopy(KGObject, OMDataCopy):
),
]
existence_query_properties = ("lookup_label",)
sort_property = "lookup_label"

def __init__(
self,
Expand Down
1 change: 1 addition & 0 deletions fairgraph/openminds/v4/computation/environment.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ class Environment(KGObject, OMEnvironment):
),
]
existence_query_properties = ("hardware", "name")
sort_property = "name"

def __init__(
self,
Expand Down
1 change: 1 addition & 0 deletions fairgraph/openminds/v4/computation/generic_computation.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ class GenericComputation(KGObject, OMGenericComputation):
),
]
existence_query_properties = ("lookup_label",)
sort_property = "lookup_label"

def __init__(
self,
Expand Down
1 change: 1 addition & 0 deletions fairgraph/openminds/v4/computation/hardware_system.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ class HardwareSystem(KGObject, OMHardwareSystem):
),
]
existence_query_properties = ("name",)
sort_property = "name"

def __init__(
self,
Expand Down
1 change: 1 addition & 0 deletions fairgraph/openminds/v4/computation/launch_configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ class LaunchConfiguration(KGObject, OMLaunchConfiguration):
]
aliases = {"environment_variables": "environment_variable"}
existence_query_properties = ("executable", "name")
sort_property = "name"

def __init__(
self,
Expand Down
1 change: 1 addition & 0 deletions fairgraph/openminds/v4/computation/local_file.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ class LocalFile(KGObject, OMLocalFile):
),
]
existence_query_properties = ("name", "hashes")
sort_property = "name"

def __init__(
self,
Expand Down
1 change: 1 addition & 0 deletions fairgraph/openminds/v4/computation/model_validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ class ModelValidation(KGObject, OMModelValidation):
),
]
existence_query_properties = ("lookup_label",)
sort_property = "lookup_label"

def __init__(
self,
Expand Down
1 change: 1 addition & 0 deletions fairgraph/openminds/v4/computation/optimization.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ class Optimization(KGObject, OMOptimization):
),
]
existence_query_properties = ("lookup_label",)
sort_property = "lookup_label"

def __init__(
self,
Expand Down
1 change: 1 addition & 0 deletions fairgraph/openminds/v4/computation/simulation.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ class Simulation(KGObject, OMSimulation):
),
]
existence_query_properties = ("lookup_label",)
sort_property = "lookup_label"

def __init__(
self,
Expand Down
1 change: 1 addition & 0 deletions fairgraph/openminds/v4/computation/software_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ class SoftwareAgent(KGObject, OMSoftwareAgent):
),
]
existence_query_properties = ("name", "software")
sort_property = "name"

def __init__(
self,
Expand Down
1 change: 1 addition & 0 deletions fairgraph/openminds/v4/computation/validation_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ class ValidationTest(KGObject, OMValidationTest):
]
aliases = {"name": "full_name", "versions": "has_versions", "model_scope": "scope", "alias": "short_name"}
existence_query_properties = ("full_name", "short_name")
sort_property = "full_name"

def __init__(
self,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ class ValidationTestVersion(KGObject, OMValidationTestVersion):
]
aliases = {"name": "full_name", "alias": "short_name"}
existence_query_properties = ("short_name", "version_identifier")
sort_property = "full_name"

def __init__(
self,
Expand Down
1 change: 1 addition & 0 deletions fairgraph/openminds/v4/computation/visualization.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ class Visualization(KGObject, OMVisualization):
),
]
existence_query_properties = ("lookup_label",)
sort_property = "lookup_label"

def __init__(
self,
Expand Down
1 change: 1 addition & 0 deletions fairgraph/openminds/v4/computation/workflow_recipe.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ class WorkflowRecipe(KGObject, OMWorkflowRecipe):
]
aliases = {"name": "full_name", "versions": "has_versions", "alias": "short_name"}
existence_query_properties = ("full_name",)
sort_property = "full_name"

def __init__(
self,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ class WorkflowRecipeVersion(KGObject, OMWorkflowRecipeVersion):
]
aliases = {"name": "full_name", "alias": "short_name"}
existence_query_properties = ("full_name", "version_identifier")
sort_property = "full_name"

def __init__(
self,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ class ActionStatusType(KGObject, OMActionStatusType):
),
]
existence_query_properties = ("name",)
sort_property = "name"

def __init__(
self,
Expand Down
1 change: 1 addition & 0 deletions fairgraph/openminds/v4/controlled_terms/age_category.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ class AgeCategory(KGObject, OMAgeCategory):
),
]
existence_query_properties = ("name",)
sort_property = "name"

def __init__(
self,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ class AnalysisTechnique(KGObject, OMAnalysisTechnique):
),
]
existence_query_properties = ("name",)
sort_property = "name"

def __init__(
self,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ class AnatomicalAxesOrientation(KGObject, OMAnatomicalAxesOrientation):
),
]
existence_query_properties = ("name",)
sort_property = "name"

def __init__(
self,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ class AnatomicalIdentificationType(KGObject, OMAnatomicalIdentificationType):
),
]
existence_query_properties = ("name",)
sort_property = "name"

def __init__(
self,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ class AnatomicalPlane(KGObject, OMAnatomicalPlane):
),
]
existence_query_properties = ("name",)
sort_property = "name"

def __init__(
self,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ class AnnotationCriteriaType(KGObject, OMAnnotationCriteriaType):
),
]
existence_query_properties = ("name",)
sort_property = "name"

def __init__(
self,
Expand Down
1 change: 1 addition & 0 deletions fairgraph/openminds/v4/controlled_terms/annotation_type.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ class AnnotationType(KGObject, OMAnnotationType):
),
]
existence_query_properties = ("name",)
sort_property = "name"

def __init__(
self,
Expand Down
1 change: 1 addition & 0 deletions fairgraph/openminds/v4/controlled_terms/atlas_type.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ class AtlasType(KGObject, OMAtlasType):
),
]
existence_query_properties = ("name",)
sort_property = "name"

def __init__(
self,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ class AuditoryStimulusType(KGObject, OMAuditoryStimulusType):
),
]
existence_query_properties = ("name",)
sort_property = "name"

def __init__(
self,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ class BiologicalOrder(KGObject, OMBiologicalOrder):
),
]
existence_query_properties = ("name",)
sort_property = "name"

def __init__(
self,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ class BiologicalProcess(KGObject, OMBiologicalProcess):
),
]
existence_query_properties = ("name",)
sort_property = "name"

def __init__(
self,
Expand Down
1 change: 1 addition & 0 deletions fairgraph/openminds/v4/controlled_terms/biological_sex.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ class BiologicalSex(KGObject, OMBiologicalSex):
),
]
existence_query_properties = ("name",)
sort_property = "name"

def __init__(
self,
Expand Down
Loading
Loading