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
  •  
  •  
  •  
1 change: 1 addition & 0 deletions builder/fairgraph_module_template.py.txt
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ class {{ class_name }}({{base_class}}, OM{{ class_name }}):
{%- if default_space %}
default_space = "{{ default_space }}"
{%- endif %}
SORT_PRIORITY = {{ sort_priority }}
# forward properties are defined in the parent class (in openMINDS-Python)
reverse_properties = [
{% for prop in reverse_properties -%}
Expand Down
19 changes: 19 additions & 0 deletions builder/update_openminds.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,24 @@
}


# The order, most preferred first, in which a generated class picks its single sort key.
# The names are the camelCase schema names used as ``QueryProperty.name`` (e.g. the Python
# attribute ``lookup_label`` is the schema name ``lookupLabel``). ``name`` is preferred because
# its values are human-readable and sort meaningfully; ``lookupLabel`` is put first only for the
# classes listed in SORT_PRIORITY_EXCEPTIONS, where it keeps terms from the same atlas/scheme
# together. ``synonyms`` is excluded: it is list-valued and not a sensible sort key.
# The Knowledge Graph API allows ``"sort": true`` on only one property, at the root level, so
# exactly one property is chosen and it is always a top-level property.
DEFAULT_SORT_PRIORITY = ("name", "fullName", "shortName", "familyName", "abbreviation", "lookupLabel")

# Per-class overrides, most preferred sort property first. The rest of the priority order is
# left unchanged (the full list is emitted into the generated class).
SORT_PRIORITY_EXCEPTIONS = {
"ParcellationEntity": ("lookupLabel", "name", "fullName", "shortName", "familyName", "abbreviation"),
"ParcellationEntityVersion": ("lookupLabel", "name", "fullName", "shortName", "familyName", "abbreviation"),
}


reverse_name_map = {
"RRID": "identifies",
"about": {
Expand Down Expand Up @@ -951,6 +969,7 @@ def get_type(prop):
"standard_init_properties": standard_init_properties,
"additional_methods": additional_methods,
"aliases": aliases,
"sort_priority": SORT_PRIORITY_EXCEPTIONS.get(class_name, DEFAULT_SORT_PRIORITY),
"constructor_arguments": sorted(
[p["name"] for p in chain(properties, reverse_properties)] + list(aliases.keys()),
key=property_name_sort_key,
Expand Down
29 changes: 29 additions & 0 deletions doc/queries.rst
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,35 @@ 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.

Result ordering
---------------

The order in which ``list()`` returns results is only meaningful when the metadata is retrieved
through the query API (i.e. when a filter or ``follow_links`` is used, or when ``api="query"``
is passed explicitly). In that case fairgraph asks the Knowledge Graph to sort the results by a
single property, in **ascending** order and **case-insensitively** (so, e.g., ``AAL1_brain`` is
placed between ``AAL1_AMYG`` and ``AAL1_CAU``, which is not the order plain Python ``sorted()``
would produce).

The Knowledge Graph API allows sorting on exactly one property, at the root level. fairgraph
chooses that property from the class's name-like properties in a fixed priority order:
``name``, then ``full_name``, ``short_name``, ``family_name``, ``abbreviation``, then
``lookup_label``. It picks the first of these that the class actually has, so, for example,
``Dataset`` is sorted by ``full_name`` and ``Person`` by ``family_name``. ``synonyms`` is never
used as a sort key (it is list-valued), and classes with none of these properties are returned in
whatever order the Knowledge Graph supplies.

A few classes override this default because a different property keeps related results together.
In particular, ``ParcellationEntity`` and ``ParcellationEntityVersion`` are sorted by
``lookup_label`` so that terms from the same atlas stay grouped.

Results retrieved through the core API (the default for a plain, unfiltered ``list()``) are
returned in an unspecified order.

.. note:: This ordering of *results* is unrelated to the internal ``ensure_order`` option on a
query property, which only preserves the order of instances within a list-valued
property, not the ordering of the results returned.


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

- Generated queries now carry exactly **one** sort key, chosen deliberately, instead of sorting
by every name-like property. The Knowledge Graph API allows ``"sort": true`` on only a single
root-level property, but fairgraph previously set it on *all* matching name-like root
properties, so classes with both ``name`` and ``lookup_label`` (e.g. ``ParcellationEntity``,
``Electrode``, ``SlicingDevice``) produced an invalid query with two sort keys. The sortable
set is now aligned with the name-like set used by :meth:`~fairgraph.kgobject.KGObject.by_name`,
and the single sort property is chosen from it in a fixed default priority order: ``name``,
then ``full_name``, ``short_name``, ``family_name``, ``abbreviation``, then ``lookup_label``
(``synonyms`` is never used). In practice:

- a class with both ``name`` and ``lookup_label`` is now sorted by ``name`` (previously
``lookup_label`` won only because it happened to precede it), e.g. ``Electrode``;
- :class:`~fairgraph.openminds.core.Dataset` and
:class:`~fairgraph.openminds.core.DatasetVersion` are sorted by ``full_name``;
- :class:`~fairgraph.openminds.core.Person` gains a ``family_name`` sort key
where it previously had none;
- classes with none of the sortable properties (e.g. ``DOI``) are returned in whatever order
the Knowledge Graph supplies.

A few classes override the default order via a per-class ``SORT_PRIORITY`` attribute so that a
different property keeps related results together: ``ParcellationEntity`` and
``ParcellationEntityVersion`` are sorted by ``lookup_label`` so that terms from the same atlas
stay grouped (instead of by ``name``). The override is defined in the builder's
``SORT_PRIORITY_EXCEPTIONS`` and regenerated into each generated class.

Results are sorted in ascending, case-insensitive order. This is unrelated to the internal
``ensure_order`` option on a query property, which only preserves the order of instances within
a list-valued property (`#129 <https://github.com/HumanBrainProject/fairgraph/issues/129>`_).

Bug fixes
---------

Expand All @@ -51,6 +80,15 @@ Bug fixes
Such a link now gives the same existence query as the object it points to
(`#145 <https://github.com/HumanBrainProject/fairgraph/issues/145>`_).

Documentation
-------------

- The order in which :meth:`~fairgraph.kgobject.KGObject.list` returns results is now documented,
in :doc:`queries` and in the :meth:`list` docstring. It covers the single sort key, its priority
order, and the ascending, case-insensitive ordering the Knowledge Graph applies, and it
distinguishes that from the internal ``ensure_order`` option (`#129 <https://github.com/HumanBrainProject/fairgraph/issues/129>`_).
A test against the pre-production Knowledge Graph pins the case-insensitive ordering.


Version 0.15.0
==============
Expand Down
24 changes: 20 additions & 4 deletions fairgraph/kgobject.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
from openminds.base import LinkedNodeEmbedding

from .utility import expand_uri, as_list, expand_filter, ActivityLog, normalize_data, handle_scope_keyword
from .queries import Query, QueryProperty, Regex
from .queries import Query, QueryProperty, Regex, choose_sort_property, SORT_PRIORITY
from .errors import AuthorizationError, ResourceExistsError, CannotBuildExistenceQuery
from .caching import object_cache, save_cache, generate_cache_key
from .base import ErrorHandling, Releasable, JSONdict
Expand Down Expand Up @@ -72,6 +72,11 @@ 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.
#
# Priority order, most preferred first, in which generate_query picks the single sort key.
# Generated openMINDS classes override this (see the builder's DEFAULT_SORT_PRIORITY /
# SORT_PRIORITY_EXCEPTIONS). If None, generate_query falls back to the default in queries.py.
SORT_PRIORITY = None

def __init__(
self,
Expand Down Expand Up @@ -395,6 +400,16 @@ def list(
Returns:
A list of instances of this class representing the objects returned by the KG query.

Note:
When the query API is used (i.e. a filter or ``follow_links`` is given, or
``api="query"`` is passed), the results are sorted in ascending, case-insensitive
order by a single name-like property chosen in a fixed priority order
(``name``, ``full_name``, ``short_name``, ``family_name``, ``abbreviation``,
``lookup_label``; ``synonyms`` is never used). A few classes override this order via
their ``SORT_PRIORITY`` class attribute (e.g. ``ParcellationEntity`` sorts by
``lookup_label`` so atlas terms stay grouped). Results from the core API are in an
unspecified order.

Raises:
ValueError: If invalid arguments are passed to the method.
NotImplementedError: If 'follow_links' is used with api='core'.
Expand Down Expand Up @@ -1183,9 +1198,10 @@ 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
sort_priority = getattr(cls, "SORT_PRIORITY", None) or SORT_PRIORITY
sort_property = choose_sort_property(query.properties, sort_priority)
if sort_property is not None:
sort_property.sorted = True
# 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/amount_of_chemical.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ class AmountOfChemical(KGEmbedded, OMAmountOfChemical):
"""

type_ = "https://openminds.om-i.org/types/AmountOfChemical"
SORT_PRIORITY = ("name", "fullName", "shortName", "familyName", "abbreviation", "lookupLabel")
# forward properties are defined in the parent class (in openMINDS-Python)
reverse_properties = []
existence_query_properties = ("chemical_product", "amount")
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 @@ -16,6 +16,7 @@ class ChemicalMixture(KGObject, OMChemicalMixture):

type_ = "https://openminds.om-i.org/types/ChemicalMixture"
default_space = "in-depth"
SORT_PRIORITY = ("name", "fullName", "shortName", "familyName", "abbreviation", "lookupLabel")
# forward properties are defined in the parent class (in openMINDS-Python)
reverse_properties = [
Property(
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 @@ -16,6 +16,7 @@ class ChemicalSubstance(KGObject, OMChemicalSubstance):

type_ = "https://openminds.om-i.org/types/ChemicalSubstance"
default_space = "in-depth"
SORT_PRIORITY = ("name", "fullName", "shortName", "familyName", "abbreviation", "lookupLabel")
# forward properties are defined in the parent class (in openMINDS-Python)
reverse_properties = [
Property(
Expand Down
1 change: 1 addition & 0 deletions fairgraph/openminds/v4/chemicals/product_source.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ class ProductSource(KGObject, OMProductSource):

type_ = "https://openminds.om-i.org/types/ProductSource"
default_space = "in-depth"
SORT_PRIORITY = ("name", "fullName", "shortName", "familyName", "abbreviation", "lookupLabel")
# forward properties are defined in the parent class (in openMINDS-Python)
reverse_properties = [
Property(
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 @@ -19,6 +19,7 @@ class DataAnalysis(KGObject, OMDataAnalysis):

type_ = "https://openminds.om-i.org/types/DataAnalysis"
default_space = "computation"
SORT_PRIORITY = ("name", "fullName", "shortName", "familyName", "abbreviation", "lookupLabel")
# forward properties are defined in the parent class (in openMINDS-Python)
reverse_properties = [
Property(
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 @@ -19,6 +19,7 @@ class DataCopy(KGObject, OMDataCopy):

type_ = "https://openminds.om-i.org/types/DataCopy"
default_space = "computation"
SORT_PRIORITY = ("name", "fullName", "shortName", "familyName", "abbreviation", "lookupLabel")
# forward properties are defined in the parent class (in openMINDS-Python)
reverse_properties = [
Property(
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 @@ -16,6 +16,7 @@ class Environment(KGObject, OMEnvironment):

type_ = "https://openminds.om-i.org/types/Environment"
default_space = "computation"
SORT_PRIORITY = ("name", "fullName", "shortName", "familyName", "abbreviation", "lookupLabel")
# forward properties are defined in the parent class (in openMINDS-Python)
reverse_properties = [
Property(
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 @@ -19,6 +19,7 @@ class GenericComputation(KGObject, OMGenericComputation):

type_ = "https://openminds.om-i.org/types/GenericComputation"
default_space = "computation"
SORT_PRIORITY = ("name", "fullName", "shortName", "familyName", "abbreviation", "lookupLabel")
# forward properties are defined in the parent class (in openMINDS-Python)
reverse_properties = [
Property(
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 @@ -16,6 +16,7 @@ class HardwareSystem(KGObject, OMHardwareSystem):

type_ = "https://openminds.om-i.org/types/HardwareSystem"
default_space = "computation"
SORT_PRIORITY = ("name", "fullName", "shortName", "familyName", "abbreviation", "lookupLabel")
# forward properties are defined in the parent class (in openMINDS-Python)
reverse_properties = [
Property(
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 @@ -16,6 +16,7 @@ class LaunchConfiguration(KGObject, OMLaunchConfiguration):

type_ = "https://openminds.om-i.org/types/LaunchConfiguration"
default_space = "computation"
SORT_PRIORITY = ("name", "fullName", "shortName", "familyName", "abbreviation", "lookupLabel")
# forward properties are defined in the parent class (in openMINDS-Python)
reverse_properties = [
Property(
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 @@ -16,6 +16,7 @@ class LocalFile(KGObject, OMLocalFile):

type_ = "https://openminds.om-i.org/types/LocalFile"
default_space = "computation"
SORT_PRIORITY = ("name", "fullName", "shortName", "familyName", "abbreviation", "lookupLabel")
# forward properties are defined in the parent class (in openMINDS-Python)
reverse_properties = [
Property(
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 @@ -20,6 +20,7 @@ class ModelValidation(KGObject, OMModelValidation):

type_ = "https://openminds.om-i.org/types/ModelValidation"
default_space = "computation"
SORT_PRIORITY = ("name", "fullName", "shortName", "familyName", "abbreviation", "lookupLabel")
# forward properties are defined in the parent class (in openMINDS-Python)
reverse_properties = [
Property(
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 @@ -19,6 +19,7 @@ class Optimization(KGObject, OMOptimization):

type_ = "https://openminds.om-i.org/types/Optimization"
default_space = "computation"
SORT_PRIORITY = ("name", "fullName", "shortName", "familyName", "abbreviation", "lookupLabel")
# forward properties are defined in the parent class (in openMINDS-Python)
reverse_properties = [
Property(
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 @@ -19,6 +19,7 @@ class Simulation(KGObject, OMSimulation):

type_ = "https://openminds.om-i.org/types/Simulation"
default_space = "computation"
SORT_PRIORITY = ("name", "fullName", "shortName", "familyName", "abbreviation", "lookupLabel")
# forward properties are defined in the parent class (in openMINDS-Python)
reverse_properties = [
Property(
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 @@ -16,6 +16,7 @@ class SoftwareAgent(KGObject, OMSoftwareAgent):

type_ = "https://openminds.om-i.org/types/SoftwareAgent"
default_space = "computation"
SORT_PRIORITY = ("name", "fullName", "shortName", "familyName", "abbreviation", "lookupLabel")
# forward properties are defined in the parent class (in openMINDS-Python)
reverse_properties = [
Property(
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 @@ -19,6 +19,7 @@ class ValidationTest(KGObject, OMValidationTest):

type_ = "https://openminds.om-i.org/types/ValidationTest"
default_space = "computation"
SORT_PRIORITY = ("name", "fullName", "shortName", "familyName", "abbreviation", "lookupLabel")
# forward properties are defined in the parent class (in openMINDS-Python)
reverse_properties = [
Property(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ class ValidationTestVersion(KGObject, OMValidationTestVersion):

type_ = "https://openminds.om-i.org/types/ValidationTestVersion"
default_space = "computation"
SORT_PRIORITY = ("name", "fullName", "shortName", "familyName", "abbreviation", "lookupLabel")
# forward properties are defined in the parent class (in openMINDS-Python)
reverse_properties = [
Property(
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 @@ -19,6 +19,7 @@ class Visualization(KGObject, OMVisualization):

type_ = "https://openminds.om-i.org/types/Visualization"
default_space = "computation"
SORT_PRIORITY = ("name", "fullName", "shortName", "familyName", "abbreviation", "lookupLabel")
# forward properties are defined in the parent class (in openMINDS-Python)
reverse_properties = [
Property(
Expand Down
1 change: 1 addition & 0 deletions fairgraph/openminds/v4/computation/workflow_execution.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ class WorkflowExecution(KGObject, OMWorkflowExecution):

type_ = "https://openminds.om-i.org/types/WorkflowExecution"
default_space = "computation"
SORT_PRIORITY = ("name", "fullName", "shortName", "familyName", "abbreviation", "lookupLabel")
# forward properties are defined in the parent class (in openMINDS-Python)
reverse_properties = []
existence_query_properties = ("stages",)
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 @@ -19,6 +19,7 @@ class WorkflowRecipe(KGObject, OMWorkflowRecipe):

type_ = "https://openminds.om-i.org/types/WorkflowRecipe"
default_space = "computation"
SORT_PRIORITY = ("name", "fullName", "shortName", "familyName", "abbreviation", "lookupLabel")
# forward properties are defined in the parent class (in openMINDS-Python)
reverse_properties = [
Property(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ class WorkflowRecipeVersion(KGObject, OMWorkflowRecipeVersion):

type_ = "https://openminds.om-i.org/types/WorkflowRecipeVersion"
default_space = "computation"
SORT_PRIORITY = ("name", "fullName", "shortName", "familyName", "abbreviation", "lookupLabel")
# forward properties are defined in the parent class (in openMINDS-Python)
reverse_properties = [
Property(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ class ActionStatusType(KGObject, OMActionStatusType):

type_ = "https://openminds.om-i.org/types/ActionStatusType"
default_space = "controlled"
SORT_PRIORITY = ("name", "fullName", "shortName", "familyName", "abbreviation", "lookupLabel")
# forward properties are defined in the parent class (in openMINDS-Python)
reverse_properties = [
Property(
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 @@ -19,6 +19,7 @@ class AgeCategory(KGObject, OMAgeCategory):

type_ = "https://openminds.om-i.org/types/AgeCategory"
default_space = "controlled"
SORT_PRIORITY = ("name", "fullName", "shortName", "familyName", "abbreviation", "lookupLabel")
# forward properties are defined in the parent class (in openMINDS-Python)
reverse_properties = [
Property(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ class AnalysisTechnique(KGObject, OMAnalysisTechnique):

type_ = "https://openminds.om-i.org/types/AnalysisTechnique"
default_space = "controlled"
SORT_PRIORITY = ("name", "fullName", "shortName", "familyName", "abbreviation", "lookupLabel")
# forward properties are defined in the parent class (in openMINDS-Python)
reverse_properties = [
Property(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ class AnatomicalAxesOrientation(KGObject, OMAnatomicalAxesOrientation):

type_ = "https://openminds.om-i.org/types/AnatomicalAxesOrientation"
default_space = "controlled"
SORT_PRIORITY = ("name", "fullName", "shortName", "familyName", "abbreviation", "lookupLabel")
# forward properties are defined in the parent class (in openMINDS-Python)
reverse_properties = [
Property(
Expand Down
Loading
Loading