Skip to content

Commit

Permalink
Initial support for generalized sort order options
Browse files Browse the repository at this point in the history
- currently only supported for lifelists
- the default sort order is `sort by name` (ascending)
- use `sort by obs` to sort life list taxa by number of observations (descending)
- use `asc` or `desc` to specify ascending or descending order
  • Loading branch information
synrg committed Jul 14, 2024
1 parent aabddd5 commit 89e59ae
Show file tree
Hide file tree
Showing 6 changed files with 93 additions and 7 deletions.
8 changes: 8 additions & 0 deletions dronefly/core/commands/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,12 @@ def life(self, ctx: Context, *args):
per_rank = query.per or "main"
if per_rank not in [*RANK_KEYWORDS, "leaf", "main", "any"]:
return "Specify `per <rank-or-keyword>`"
sort_by = query.sort_by or None
if sort_by not in ["obs", "name"]:
return "Specify `sort by obs` or `sort by name` (default)"
order = query.order or None
if order not in [None, "asc", "desc"]:
return "Specify `order asc` or `order desc`"

query_args = get_base_query_args(query)
with self.inat_client.set_ctx(ctx) as client:
Expand Down Expand Up @@ -187,6 +193,8 @@ def life(self, ctx: Context, *args):
with_indent=True,
per_page=per_page,
with_index=with_index,
sort_by=sort_by,
order=order,
)
ctx.page_formatter = formatter
ctx.page = 0
Expand Down
81 changes: 75 additions & 6 deletions dronefly/core/formatters/generic.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from __future__ import annotations
import copy
from datetime import datetime as dt
import functools
from math import ceil
import re
from typing import TYPE_CHECKING, Optional, Union
Expand Down Expand Up @@ -134,6 +135,8 @@ def taxa_per_rank(
life_list: LifeList,
ranks_to_count: Union[list[str], str],
root_taxon_id: int = None,
sort_by: str = None,
order: str = None,
):
"""Generate taxa matching ranks to count in treewise order."""
include_leaves = False
Expand All @@ -147,7 +150,46 @@ def taxa_per_rank(
else:
# single rank case:
include_ranks = [ranks_to_count]
tree = make_tree(life_list.data, include_ranks=include_ranks, root_id=root_taxon_id)

def _sort_rank_name(order):
"""Generate a sort key in `order` by rank and name."""

def reverse_taxon_name(taxon):
reverse_key = functools.cmp_to_key(lambda a, b: (a < b) - (a > b))
return reverse_key(taxon.name)

def sort_key(taxon):
taxon_name_key = (
reverse_taxon_name(taxon) if order == "desc" else taxon.name
)
return (taxon.rank_level or 0) * -1, taxon_name_key

return sort_key

def _sort_rank_obs_name(order):
"""Generate a sort key in `order` by rank, descendant obs count, and name."""

def sort_key(taxon):
_order = 1 if order == "asc" else -1
return (
(taxon.rank_level or 0) * -1,
taxon.descendant_obs_count * _order,
taxon.name,
)

return sort_key

# generate a sort key that uses the specified order:
sort_key = (
_sort_rank_obs_name(order) if sort_by == "obs" else _sort_rank_name(order)
)

tree = make_tree(
life_list.data,
include_ranks=include_ranks,
root_id=root_taxon_id,
sort_key=sort_key,
)
hide_root = (
tree.id == ROOT_TAXON_ID
or include_ranks
Expand Down Expand Up @@ -194,7 +236,12 @@ def included_ranks(per_rank):


def filter_life_list(
life_list: LifeList, per_rank: str, taxon: Taxon, root_taxon_id: int = None
life_list: LifeList,
per_rank: str,
taxon: Taxon,
root_taxon_id: int = None,
sort_by: str = None,
order: str = None,
):
ranks = None
rank_totals = {}
Expand All @@ -210,14 +257,20 @@ def filter_life_list(
else:
ranks_to_count = ranks_to_count[: ranks_to_count.index(taxon.rank) + 1]
ranks = "main ranks" if per_rank == "main" else "ranks"
generate_taxa = taxa_per_rank(life_list, ranks_to_count, root_taxon_id)
generate_taxa = taxa_per_rank(
life_list, ranks_to_count, root_taxon_id, sort_by, order
)
elif per_rank == "leaf":
ranks = "leaf taxa"
generate_taxa = taxa_per_rank(life_list, per_rank, root_taxon_id)
generate_taxa = taxa_per_rank(
life_list, per_rank, root_taxon_id, sort_by, order
)
else:
rank = RANK_EQUIVALENTS[per_rank] if per_rank in RANK_EQUIVALENTS else per_rank
ranks = p.plural_noun(rank)
generate_taxa = taxa_per_rank(life_list, per_rank, root_taxon_id)
generate_taxa = taxa_per_rank(
life_list, per_rank, root_taxon_id, sort_by, order
)
counted_taxa = []
counted_taxon_ids = []
tot = {}
Expand Down Expand Up @@ -625,6 +678,8 @@ def __init__(
with_common: bool = False,
per_page: int = 20,
root_taxon_id: int = None,
sort_by: str = None,
order: str = None,
):
"""
Parameters
Expand Down Expand Up @@ -680,6 +735,13 @@ def __init__(
root_taxon_id: int, optional
If specified, make the taxon with this ID the root. The taxon with
this ID must be in the life list data.
sort_by: str, optional
If specified, sort ascending by `name` (default) or descending by number of `obs`.
order: str, optional
If specified, use `asc` (ascending) or `desc` (descending) as the order for the
`sort_by` key.
"""
self.life_list = life_list
self.per_rank = per_rank
Expand All @@ -693,6 +755,8 @@ def __init__(
self.pages = []
self.per_page = per_page if per_page >= 0 else 0
self.root_taxon_id = root_taxon_id
self.sort_by = sort_by
self.order = order
(
self.taxa,
self.taxon_ids,
Expand All @@ -701,7 +765,12 @@ def __init__(
self.count_digits,
self.direct_digits,
) = filter_life_list(
self.life_list, self.per_rank, self.query_response.taxon, self.root_taxon_id
self.life_list,
self.per_rank,
self.query_response.taxon,
self.root_taxon_id,
self.sort_by,
self.order,
)

def format(
Expand Down
3 changes: 3 additions & 0 deletions dronefly/core/parsers/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@
"rank": {"nargs": "+", "dest": "ranks", "default": []},
"with": {"nargs": "+", "dest": "controlled_term"},
"per": {"nargs": "+", "dest": "per", "default": []},
"sort-by": {"nargs": "+", "dest": "sort_by", "default": []},
"asc": {"dest": "order", "action": "store_const", "const": "asc"},
"desc": {"dest": "order", "action": "store_const", "const": "desc"},
"opt": {"nargs": "+", "dest": "options", "default": []},
"in-prj": {"nargs": "+", "dest": "project", "default": []},
"since": {"nargs": "+", "dest": "obs_d1", "default": []},
Expand Down
2 changes: 1 addition & 1 deletion dronefly/core/parsers/natural.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ def parse_group(tok, groups, params={}, opts=[], expanded_tokens=[]):

try:
arg_normalized = re.sub(
r"((^| )(id|not|except)) ?by ", r"\2\3-by ", argument, re.I
r"((^| )(id|not|except|sort)) ?by ", r"\2\3-by ", argument, re.I
)
arg_normalized = re.sub(
r"((^| )in ?prj) ", r"\2in-prj ", arg_normalized, re.I
Expand Down
2 changes: 2 additions & 0 deletions dronefly/core/parsers/unixlike.py
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,8 @@ def parse(self, argument: str):
id_by=" ".join(vals.id_by),
per=" ".join(vals.per),
project=" ".join(vals.project),
sort_by=" ".join(vals.sort_by),
order=vals.order,
options=vals.options,
obs_d1=obs_d1,
obs_d2=obs_d2,
Expand Down
4 changes: 4 additions & 0 deletions dronefly/core/query/query.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,8 @@ class Query:
except_by: Optional[str] = None
id_by: Optional[str] = None
per: Optional[str] = None
sort_by: Optional[str] = None
order: Optional[str] = None
project: Optional[str] = None
options: Optional[List] = None
obs_d1: Optional[List] = None
Expand Down Expand Up @@ -162,6 +164,8 @@ def __str__(self):
self._add_clause("added since {}", self.added_d1)
self._add_clause("added until {}", self.added_d2)
self._add_clause("added on {}", self.added_on)
self._add_clause("sorted by {}", self.sort_by)
self._add_clause("({self.order})", self.order)
return self._query


Expand Down

0 comments on commit 89e59ae

Please sign in to comment.