Non-aphabetic ordering of templated dict items

I’m pulling a bunch of dicts from an API, each of which looks something like the following after piping through to_nice_yaml:

  - createdAt: '2022-01-04T16:31:35Z'
    executionOrder: Parallel
    frequency: Weekly
    frequencyDetails:
      intervals:
        interval:
        - weekDay: Monday
        - weekDay: Tuesday
        - weekDay: Wednesday
        - weekDay: Thursday
        - weekDay: Friday
      start: 02:00:00
    id: 75d85156-2e01-47a2-b3df-4a3cf7d43fed
    name: Run Flow - Weekday 2:00AM
    nextRunAt: '2025-11-04T07:00:00Z'
    priority: '50'
    state: Active
    type: Flow
    updatedAt: '2025-11-03T07:00:26Z'

(These are Tableau™ job queue schedules, btw, if you’re curious. But that’s beside the point.)
Some of these dicts may have more or fewer attributes than shown above.

The problem: I want to template these dicts with a specific non-alphabetic order of top-level attributes. Particularly, name, type, and state should be listed first, in that order (if they exist), followed by any other existing attributes in no particular order. A bonus would be the ability to skip some attributes —nextRunAt, createdAt, and updatedAt for example. I don’t care about the ordering of “subordinate” attributes like the internals of frequencyDetails in this case.

My Question: Is there an elegant way of filtering or otherwise iterating over dict attributes to meet such ordering requirements? I know there are inelegant ways to do it; I’m using one now, and it’s xxxx-ugly, full of brute force Jinja tricks that are not particularly robust when faced with dicts having missing or unexpected extra attributes. I realize the ordering doesn’t matter to machines, but humans who have to read these things would appreciate having, for example, the name at the top.

Suggestions?

I think any pure jinja implementation is going to be problematic, especially from a “wtf is this doing” perspective.

I’d recommend a custom filter. Potentially that takes a priority list. Something like:

def prio_sort(v, keys=None):
    if not keys:
        prio_map = {}
    else:
        prio_map = {k: i for i, k in enumerate(keys)}

    return dict(sorted(v.items(), key=lambda k_v: prio_map.get(k_v[0], 65535)))


class FilterModule:
    def filters(self):
        return {
            'prio_sort': prio_sort,
        }

Then in your playbook, or wherever you use it:

things | map('prio_sort', keys=['name', 'type', 'state']) | to_nice_yaml(sort_keys=False)

The default for to_nice_yaml is sort_keys=True which would destroy the sorting just done, so you have to disable it.

Note that the default callback (by default) will use json.dumps with sort_keys=True, so depending on how you look at it, without the to_nice_yaml(sort_keys=False) it would potentially appear as though the sorting didn’t work.

2 Likes

Thank you so much, @sivel. That’s exactly what I was hoping for, but my python foo is not likely to have come up with such a clean implementation. (I learned BASIC in 1978. Everything I’ve written since then looks like BASIC.)

I tweaked it a bit to fit our naming schemes, and added the bonus feature of omitting undesired keys. The final result is this:

def dict_reorder(v, first_keys=None, skip_keys=[]):
    if not first_keys:
        prio_map = {}
    else:
        prio_map = {k: i for i, k in enumerate(first_keys)}

    return dict(
        sorted(
            [item for item in v.items() if item[0] not in skip_keys],
            key=lambda k_v: prio_map.get(k_v[0], 65535),
        )
    )

class FilterModule:
    def filters(self):
        return {
            'dict_reorder': dict_reorder,
        }

That drops right into our local.tablinx collection’s existing plugins/filter/tablinx.py file, and my invocation looks like this:

---
# ./group_vars/mw_tablinx_{{ mw_tablinx_env }}/schedules_live.yml
#
# Generated {{ now() }} by playbook mw_tablinx_utoddl_sched_2.yml.
#
# This file contains all schedules that existed in the {{ mw_tablinx_env }}
# environment at the time the playbook was run. It will be overridden
# on subsequent runs, so don't edit this file.
mw_tablinx_schedules_live_{{ mw_tablinx_env }}:
{% for sch in schedules_all | sort(attribute="state,type,name") %}
  - {{ sch | local.tablinx.dict_reorder(first_keys=['name', 'type', 'state'],
                                        skip_keys=['nextRunAt', 'createdAt', 'updatedAt'])
           | to_nice_yaml(indent=2, sort_keys=False)
           | indent(4) }}
{% endfor %} 

That’s still not tiny, but each bit of the Jinja does something comprehensible — which is a great improvement over the fiasco I was creating.

I am very happy! :star_struck: