Skip to content
Closed
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
  •  
  •  
  •  
5 changes: 5 additions & 0 deletions builder/fairgraph_module_template.py.txt
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ class {{ class_name }}({{base_class}}, OM{{ class_name }}):
{%- if default_space %}
default_space = "{{ default_space }}"
{%- endif %}
{%- if sort_property %}
SORT_PROPERTY = "{{ sort_property }}"
{%- else %}
SORT_PROPERTY = None
{%- endif %}
# forward properties are defined in the parent class (in openMINDS-Python)
reverse_properties = [
{% for prop in reverse_properties -%}
Expand Down
38 changes: 38 additions & 0 deletions builder/update_openminds.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,25 @@
}


# 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.
#
# These priorities are evaluated once, at build time, to decide for each class which single
# top-level property to sort by (see get_sort_property). The Knowledge Graph API allows
# ``"sort": true`` on only one property, at the root level, so at most one property is chosen.
DEFAULT_SORT_PRIORITY = ("name", "fullName", "shortName", "familyName", "abbreviation", "lookupLabel")

# Per-class overrides of the sort priority, most preferred sort property first.
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 @@ -682,6 +701,24 @@ def get_default_space(schema_group, cls_name, version="v4"):
}


def get_sort_property(cls_name, schema_properties):
"""Return the single sort property for a class, or ``None``.

Decided once, at build time, using the priority in DEFAULT_SORT_PRIORITY (or its per-class
override in SORT_PRIORITY_EXCEPTIONS): the first name-like property the class actually has,
in priority order, is chosen and emitted into the generated class as ``SORT_PROPERTY``.
``schema_properties`` is the raw openMINDS schema ``properties`` dict (keyed by full property
IRI; each value's ``name`` is the camelCase schema name, which is what ``QueryProperty.name``
uses at runtime). Returns ``None`` when the class has none of the sortable properties.
"""
priority = SORT_PRIORITY_EXCEPTIONS.get(cls_name, DEFAULT_SORT_PRIORITY)
present = {prop["name"] for prop in schema_properties.values()}
for name in priority:
if name in present:
return name
return None


def get_existence_query(cls_name, properties):
if cls_name in custom_existence_queries:
return custom_existence_queries[cls_name]
Expand Down Expand Up @@ -951,6 +988,7 @@ def get_type(prop):
"standard_init_properties": standard_init_properties,
"additional_methods": additional_methods,
"aliases": aliases,
"sort_property": get_sort_property(class_name, self._schema_payload["properties"]),
"constructor_arguments": sorted(
[p["name"] for p in chain(properties, reverse_properties)] + list(aliases.keys()),
key=property_name_sort_key,
Expand Down
31 changes: 31 additions & 0 deletions doc/queries.rst
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,37 @@ 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. This decision is made once, at build time, and the
chosen property is baked into each generated class as its ``SORT_PROPERTY`` attribute (see the
builder's ``DEFAULT_SORT_PRIORITY`` / ``SORT_PRIORITY_EXCEPTIONS`` in ``builder/update_openminds.py``).

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
41 changes: 41 additions & 0 deletions doc/release_notes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,38 @@ 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 via a per-class override 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``). Which property to sort by is decided **once, at build
time** (in ``builder/update_openminds.py``, see ``DEFAULT_SORT_PRIORITY`` and
``SORT_PRIORITY_EXCEPTIONS``) and baked into each generated class as its ``SORT_PROPERTY``
attribute — the runtime simply applies that single value instead of evaluating a priority
order for every query.

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 +83,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
27 changes: 22 additions & 5 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, find_sort_property
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,10 @@ 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.
# The name-like property to sort a generated query by, or None for no sort. This is set at
# build time on the generated openMINDS classes (see the builder's DEFAULT_SORT_PRIORITY /
# SORT_PRIORITY_EXCEPTIONS) and used by generate_query.
SORT_PROPERTY = None

def __init__(
self,
Expand Down Expand Up @@ -395,6 +399,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. Which property is used is decided at build time:
by default it follows the priority ``name``, ``full_name``, ``short_name``,
``family_name``, ``abbreviation``, ``lookup_label`` (``synonyms`` is never used), but
some classes override it via their ``SORT_PROPERTY`` 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 @@ -1182,10 +1196,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 can only happen at the top level; which property to
# sort by is decided at build time and baked into SORT_PROPERTY on the generated class.
sort_property_name = getattr(cls, "SORT_PROPERTY", None)
if sort_property_name is not None:
sort_property = find_sort_property(query.properties, sort_property_name)
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_PROPERTY = None
# 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_PROPERTY = "name"
# 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_PROPERTY = "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_PROPERTY = None
# 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_PROPERTY = "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_PROPERTY = "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_PROPERTY = "name"
# 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_PROPERTY = "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_PROPERTY = "name"
# 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_PROPERTY = "name"
# 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_PROPERTY = "name"
# 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_PROPERTY = "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_PROPERTY = "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_PROPERTY = "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_PROPERTY = "name"
# 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_PROPERTY = "fullName"
# 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_PROPERTY = "fullName"
# 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_PROPERTY = "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_PROPERTY = None
# 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_PROPERTY = "fullName"
# 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_PROPERTY = "fullName"
# 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_PROPERTY = "name"
# forward properties are defined in the parent class (in openMINDS-Python)
reverse_properties = [
Property(
Expand Down
Loading