diaryfolio
diaryfolio

Reputation: 625

Jinja - How to pass on dynamic range to an expression?

I've a csv file which i'm reading as a dict using Ansible read_csv

id,name,quantity,type
1,apple,10,fruit
2,orange,20,fruit
3,carrot,5,veg
4,beetroot,2,veg
5,onion,3,veg
6,tomato,4,both
7,pear,4,fruit
8,banana,6,fruit
9,persimon,4,fruit
10,guava,4,fruit
11,pepper,4,veg
12,potato,5,veg
13,cherry,5,fruit

The equivalent dict becomes

"{'1': {'id': '1', 'name': 'apple', 'quantity': '10', 'type': 'fruit'}, '2': {'id': '2', 'name': 'orange', 'quantity': '20', 'type': 'fruit'}, '3': {'id': '3', 'name': 'carrot', 'quantity': '5', 'type': 'veg'}, '4': {'id': '4', 'name': 'beetroot', 'quantity': '2', 'type': 'veg'}, '5': {'id': '5', 'name': 'onion', 'quantity': '3', 'type': 'veg'}, '6': {'id': '6', 'name': 'tomato', 'quantity': '4', 'type': 'both'}, '7': {'id': '7', 'name': 'pear', 'quantity': '4', 'type': 'fruit'}, '8': {'id': '8', 'name': 'banana', 'quantity': '6', 'type': 'fruit'}, '9': {'id': '9', 'name': 'persimon', 'quantity': '4', 'type': 'fruit'}, '10': {'id': '10', 'name': 'guava', 'quantity': '4', 'type': 'fruit'}, '11': {'id': '11', 'name': 'pepper', 'quantity': '4', 'type': 'veg'}, '12': {'id': '12', 'name': 'potato', 'quantity': '5', 'type': 'veg'}, '13': {'id': '13', 'name': 'cherry', 'quantity': '5', 'type': 'fruit'}}"

My logic was to split the list into batches of 2 name per type at a time So output I was looking for is ["carrot", "beetroot"], ["onion", "pepper"] and so on

The below logic works perfectly when i hardcode the [0:2] range in the jinja expression

{% set my_fruit_list = [] %}
{%- for item in (fruits_dict.dict| dict2items | selectattr("value.type", "match", "^veg$"))[0:2]  -%}
{{ my_fruit_list.append(item.value.name) }}
{%- endfor -%}
my_list=["{{ my_fruit_list|join('", "') }}"]

But when i try changing it to a dynamic variable, it doesn't work. I tried below

{% set input_range = "[0:2]" %}
{%- for item in (fruits_dict.dict| dict2items | selectattr("value.type", "match", "^veg$"))input_range  -%}

Is there any way we can pass the "input_range" as a dynamic parameter into the expression?

Also is there a better way of getting the selectattr without converting the csv to dict and dict2items ?

Upvotes: 2

Views: 255

Answers (2)

Vladimir Botka
Vladimir Botka

Reputation: 68074

Given the file

shell> cat /tmp/fruits.csv 
id,name,quantity,type
1,apple,10,fruit
2,orange,20,fruit
3,carrot,5,veg
4,beetroot,2,veg
5,onion,3,veg
6,tomato,4,both
7,pear,4,fruit
8,banana,6,fruit
9,persimon,4,fruit
10,guava,4,fruit
11,pepper,4,veg
12,potato,5,veg
13,cherry,5,fruit

Read the file and create a dictionary by the key id

    - read_csv:
        path: /tmp/fruits.csv
        key: id
      register: fruits

gives

  fruits.dict:
    '1': {id: '1', name: apple, quantity: '10', type: fruit}
    '10': {id: '10', name: guava, quantity: '4', type: fruit}
    '11': {id: '11', name: pepper, quantity: '4', type: veg}
    '12': {id: '12', name: potato, quantity: '5', type: veg}
    '13': {id: '13', name: cherry, quantity: '5', type: fruit}
    '2': {id: '2', name: orange, quantity: '20', type: fruit}
    '3': {id: '3', name: carrot, quantity: '5', type: veg}
    '4': {id: '4', name: beetroot, quantity: '2', type: veg}
    '5': {id: '5', name: onion, quantity: '3', type: veg}
    '6': {id: '6', name: tomato, quantity: '4', type: both}
    '7': {id: '7', name: pear, quantity: '4', type: fruit}
    '8': {id: '8', name: banana, quantity: '6', type: fruit}
    '9': {id: '9', name: persimon, quantity: '4', type: fruit}

  1. Get the values and group them by type
  by_type: "{{ fruits.dict.values()|groupby('type') }}"

gives

  by_type:
    - - both
      - - {id: '6', name: tomato, quantity: '4', type: both}
    - - fruit
      - - {id: '1', name: apple, quantity: '10', type: fruit}
        - {id: '2', name: orange, quantity: '20', type: fruit}
        - {id: '7', name: pear, quantity: '4', type: fruit}
        - {id: '8', name: banana, quantity: '6', type: fruit}
        - {id: '9', name: persimon, quantity: '4', type: fruit}
        - {id: '10', name: guava, quantity: '4', type: fruit}
        - {id: '13', name: cherry, quantity: '5', type: fruit}
    - - veg
      - - {id: '3', name: carrot, quantity: '5', type: veg}
        - {id: '4', name: beetroot, quantity: '2', type: veg}
        - {id: '5', name: onion, quantity: '3', type: veg}
        - {id: '11', name: pepper, quantity: '4', type: veg}
        - {id: '12', name: potato, quantity: '5', type: veg}
  1. Create the dictionary
  my_dict: "{{ dict(by_type|map('first')|list|
                    zip(by_type|map('last')|
                                map('map', attribute='name')|list)) }}"

gives

  my_dict:
    both: [tomato]
    fruit: [apple, orange, pear, banana, persimon, guava, cherry]
    veg: [carrot, beetroot, onion, pepper, potato]
  1. Create batches of the items. Where type and input_range are parameters
    my_list: |
      {% for batch in my_dict[type]|batch(input_range|int) %}
        - {{ batch }}
      {% endfor %}

For example, the loop below

    - debug:
        msg: "{{ my_list }}"
      loop: "{{ my_dict.keys()|list }}"
      vars:
        type: "{{ item }}"

for input_range=2 gives

TASK [debug] **************************************************************
ok: [localhost] => (item=both) => 
  msg: |2-
      - ['tomato']
ok: [localhost] => (item=fruit) => 
  msg: |2-
      - ['apple', 'orange']
      - ['pear', 'banana']
      - ['persimon', 'guava']
      - ['cherry']
ok: [localhost] => (item=veg) => 
  msg: |2-
      - ['carrot', 'beetroot']
      - ['onion', 'pepper']
      - ['potato']

for input_range=3 gives

TASK [debug] **************************************************************
ok: [localhost] => (item=both) => 
  msg: |2-
      - ['tomato']
ok: [localhost] => (item=fruit) => 
  msg: |2-
      - ['apple', 'orange', 'pear']
      - ['banana', 'persimon', 'guava']
      - ['cherry']
ok: [localhost] => (item=veg) => 
  msg: |2-
      - ['carrot', 'beetroot', 'onion']
      - ['pepper', 'potato']
  1. Create specific lists
    - set_fact:
        my_list_veg: "{{ my_list }}"
      vars:
        type: veg
        input_range: 3
    - debug:
        var: my_list_veg

gives

  my_list_veg:
      - ['carrot', 'beetroot', 'onion']
      - ['pepper', 'potato']

  • Example of a complete playbook for testing
shell> cat pb.yml
- hosts: localhost
  vars:
    input_range: 2
    by_type: "{{ fruits.dict.values()|groupby('type') }}"
    my_dict: "{{ dict(by_type|map('first')|list|
                      zip(by_type|map('last')|
                                  map('map', attribute='name')|list)) }}"
    my_list: |
      {% for batch in my_dict[type]|batch(input_range|int) %}
        - {{ batch }}
      {% endfor %}
      
  tasks:
    - read_csv:
        path: /tmp/fruits.csv
        key: id
      register: fruits
    - debug:
        var: fruits.dict|to_yaml
    - debug:
        var: by_type|to_yaml
    - debug:
        var: my_dict|to_yaml

    - debug:
        msg: "{{ my_list }}"
      loop: "{{ my_dict.keys()|list }}"
      vars:
        type: "{{ item }}"

    - set_fact:
        my_list_veg: "{{ my_list }}"
      vars:
        type: veg
        input_range: 3
    - debug:
        var: my_list_veg
  • You can override input_range from the command line, e.g.
shell> ansible-playbook pb.yml -e input_range=3
  • Q: "I needed a combination of 'name'-'type'-'id' as my 'my_list'"

A: Create a dictionary with all attributes

  my_dict: "{{ dict(by_type) }}"

gives

  my_dict:
    both:
    - {id: '6', name: tomato, quantity: '4', type: both}
    fruit:
    - {id: '1', name: apple, quantity: '10', type: fruit}
    - {id: '2', name: orange, quantity: '20', type: fruit}
    - {id: '7', name: pear, quantity: '4', type: fruit}
    - {id: '8', name: banana, quantity: '6', type: fruit}
    - {id: '9', name: persimon, quantity: '4', type: fruit}
    - {id: '10', name: guava, quantity: '4', type: fruit}
    - {id: '13', name: cherry, quantity: '5', type: fruit}
    veg:
    - {id: '3', name: carrot, quantity: '5', type: veg}
    - {id: '4', name: beetroot, quantity: '2', type: veg}
    - {id: '5', name: onion, quantity: '3', type: veg}
    - {id: '11', name: pepper, quantity: '4', type: veg}
    - {id: '12', name: potato, quantity: '5', type: veg}

Then, select the attributes you want. For example, select the attributes id and name and create the template

    my_list: |
      {% for batch in my_dict[type]|batch(input_range|int) %}
      {% for i in batch %}
      - id: {{ i.id }}
        name: {{ i.name }}
      {% endfor %}
      {% endfor %}

Then create specific lists, for example

    - set_fact:
        my_list_veg: "{{ my_list|from_yaml }}"
      vars:
        type: veg
        input_range: 3
    - debug:
        var: my_list_veg

gives

  my_list_veg:
    - {id: 3, name: carrot}
    - {id: 4, name: beetroot}
    - {id: 5, name: onion}
    - {id: 11, name: pepper}
    - {id: 12, name: potato}

Upvotes: 1

Zeitounator
Zeitounator

Reputation: 44615

In a nutshell:

{% set my_fruit_list = [] %}
{% set fruit_start = 0 %}
{% set fruit_end = 1 %}
{%- for item in (fruits_dict.dict| dict2items | selectattr("value.type", "match", "^veg$"))[fruit_start:fruit_end]  -%}
{{ my_fruit_list.append(item.value.name) }}
{%- endfor -%}
my_list=["{{ my_fruit_list|join('", "') }}"]

Upvotes: 1

Related Questions