Source code for braincell.io.neuromorpho.query

# Copyright 2026 BrainX Ecosystem Limited. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ==============================================================================

"""Typed query builder for NeuroMorpho.Org Solr search."""



from dataclasses import dataclass, field
from typing import Iterable

__all__ = ["NeuroMorphoQuery"]


def _normalize(value: str | Iterable[str] | None) -> tuple[str, ...]:
    if value is None:
        return ()
    if isinstance(value, str):
        return (value,)
    return tuple(str(item) for item in value)


def _build_or_clause(field_name: str, values: tuple[str, ...]) -> str | None:
    if not values:
        return None
    if len(values) == 1:
        return f"{field_name}:{values[0]}"
    joined = " OR ".join(f"{field_name}:{value}" for value in values)
    return f"({joined})"


[docs] @dataclass(frozen=True) class NeuroMorphoQuery: """Typed builder for NeuroMorpho.Org search queries. Each non-``raw`` field becomes a Solr clause that is ANDed together. Multi-value fields produce ``(field:a OR field:b)`` clauses. Use :attr:`raw_q` and :attr:`raw_fq` to append filter strings verbatim when you need a Solr feature this builder doesn't expose. Parameters ---------- species : str or tuple of str or None Filter on the ``species`` field. brain_region : str or tuple of str or None Filter on the ``brain_region`` field. cell_type : str or tuple of str or None Filter on the ``cell_type`` field. archive : str or tuple of str or None Filter on the ``archive`` field. original_format : str or tuple of str or None Filter on the ``original_format`` field. stain : str or tuple of str or None Filter on the ``stain`` field. age_classification : str or tuple of str or None Filter on the ``age_classification`` field. gender : str or tuple of str or None Filter on the ``gender`` field. raw_q : tuple of str Extra clauses appended verbatim to the ``q`` parameter. raw_fq : tuple of str Extra filter strings appended verbatim to ``fq``. Examples -------- .. code-block:: python >>> q = NeuroMorphoQuery(species="mouse", brain_region="cerebellum") >>> q.to_q() 'species:mouse AND brain_region:cerebellum' >>> q.to_fq() [] >>> multi = NeuroMorphoQuery(species=("mouse", "rat")) >>> multi.to_q() '(species:mouse OR species:rat)' """ species: str | tuple[str, ...] | None = None brain_region: str | tuple[str, ...] | None = None cell_type: str | tuple[str, ...] | None = None archive: str | tuple[str, ...] | None = None original_format: str | tuple[str, ...] | None = None stain: str | tuple[str, ...] | None = None age_classification: str | tuple[str, ...] | None = None gender: str | tuple[str, ...] | None = None raw_q: tuple[str, ...] = field(default_factory=tuple) raw_fq: tuple[str, ...] = field(default_factory=tuple)
[docs] def to_q(self) -> str: """Return the assembled ``q`` parameter for ``/api/neuron/select``. Returns ------- str ``"*:*"`` when no filters were specified, otherwise an ``AND``-joined string of clauses. """ clauses: list[str] = [] for attr in ( "species", "brain_region", "cell_type", "archive", "original_format", "stain", "age_classification", "gender", ): clause = _build_or_clause(attr, _normalize(getattr(self, attr))) if clause is not None: clauses.append(clause) clauses.extend(item for item in self.raw_q if item) if not clauses: return "*:*" return " AND ".join(clauses)
[docs] def to_fq(self) -> list[str]: """Return the ``fq`` filter list for ``/api/neuron/select``. Currently only ``raw_fq`` populates this list; the typed fields all flow through ``q``. The method exists so callers and the CLI can pass through ``fq`` strings without special-casing. Returns ------- list of str """ return [item for item in self.raw_fq if item]
[docs] def to_params(self) -> dict[str, object]: """Return ``q``/``fq`` packaged as a kwargs dict. Returns ------- dict ``{"q": ..., "fq": [...]}`` ready to be unpacked into :meth:`NeuroMorphoClient.search`. """ return {"q": self.to_q(), "fq": self.to_fq()}