Skip to content

Templates

Neon +1.3.0 +2.8.25

You can access the response data entered into the template questionnaire on the phone and also set those responses remotely. If the template is properly configured, this allows you to also define the recording name.

Get Template Definition

Using the device.get_template method, you can receive the definition of the template containing all questions and sections.

        template = await device.get_template()
Template

Template

Template Class for data collection.

Methods:

Attributes:

archived_at class-attribute instance-attribute

archived_at: datetime | None = None

Archival timestamp (if archived).

created_at instance-attribute

created_at: datetime

Creation timestamp.

description class-attribute instance-attribute

description: str | None = None

Template description.

id instance-attribute

id: UUID

Unique identifier.

is_default_template class-attribute instance-attribute

is_default_template: bool = True

Whether this is the default template for the Workspace

items class-attribute instance-attribute

items: list[TemplateItem] = field(default_factory=list)

List of template items.

label_ids class-attribute instance-attribute

label_ids: list[UUID] = field(default_factory=list, metadata={'readonly': True})

Associated label IDs.

name instance-attribute

name: str

Template name.

published_at class-attribute instance-attribute

published_at: datetime | None = None

Publication timestamp.

recording_ids class-attribute instance-attribute

recording_ids: list[UUID] | None = None

Associated recording IDs.

recording_name_format instance-attribute

recording_name_format: list[str]

Format for recording name.

updated_at instance-attribute

updated_at: datetime

Last update timestamp.

convert_from_api_to_simple_format

convert_from_api_to_simple_format(data: dict[str, list[str]]) -> dict[str, Any]

Convert data from API format to simple format.

Parameters:

Returns:

  • dict ( dict[str, Any] ) –

    Data in simple format.

Source code in src/pupil_labs/realtime_api/models.py
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
def convert_from_api_to_simple_format(
    self, data: dict[str, list[str]]
) -> dict[str, Any]:
    """Convert data from API format to simple format.

    Args:
        data: Data in API format.

    Returns:
        dict: Data in simple format.

    """
    simple_format = {}
    for question_id, value in data.items():
        question = self.get_question_by_id(question_id)
        if question is None:
            logger.warning(
                f"Skipping unknown question ID '{question_id}' during API to "
                f"simple conversion."
            )
            continue
        processed_value: Any
        if question.widget_type in {"CHECKBOX_LIST", "RADIO_LIST"}:
            if question.choices is None:
                logger.warning(
                    f"Question {question_id} (type {question.widget_type}) "
                    f"has no choices defined."
                )
                processed_value = []
            elif value == [""] and "" not in question.choices:
                processed_value = []
        else:
            if not value:
                value = [""]

            value_str = value[0]
            if question.input_type != "any":
                processed_value = (
                    None if value_str == "" else question._value_type(value_str)
                )
            else:
                processed_value = value

        simple_format[question_id] = processed_value
    return simple_format

convert_from_simple_to_api_format

convert_from_simple_to_api_format(data: dict[str, Any]) -> dict[str, list[Any]]

Convert data from simple format to API format.

Parameters:

  • data (dict[str, Any]) –

    Data in simple format.

Returns:

Source code in src/pupil_labs/realtime_api/models.py
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
def convert_from_simple_to_api_format(
    self, data: dict[str, Any]
) -> dict[str, list[Any]]:
    """Convert data from simple format to API format.

    Args:
        data: Data in simple format.

    Returns:
        dict: Data in API format.

    """
    api_format = {}
    for question_id, value in data.items():
        if value is None:
            value = ""
        if not isinstance(value, list):
            value = [value]

        api_format[question_id] = value
    return api_format

get_question_by_id

get_question_by_id(question_id: str | UUID) -> TemplateItem | None

Get a template item by ID.

Parameters:

  • question_id (str | UUID) –

    ID of the template item.

Returns:

  • TemplateItem | None

    TemplateItem | None: The template item, or None if not found.

Source code in src/pupil_labs/realtime_api/models.py
775
776
777
778
779
780
781
782
783
784
785
786
787
788
def get_question_by_id(self, question_id: str | UUID) -> TemplateItem | None:
    """Get a template item by ID.

    Args:
        question_id: ID of the template item.

    Returns:
        TemplateItem | None: The template item, or None if not found.

    """
    for item in self.items:
        if str(item.id) == str(question_id):
            return item
    return None

validate_answers

validate_answers(answers: dict[str, list[str]], template_format: TemplateDataFormat, raise_exception: bool = True) -> list[ErrorDetails]

Validate answers for this Template.

Parameters:

  • answers (dict[str, list[str]]) –

    Answers to validate.

  • raise_exception (bool, default: True ) –

    Whether to raise an exception on validation failure.

  • template_format (TemplateDataFormat) –

    Format of the answers ("simple" or "api").

Returns:

  • list ( list[ErrorDetails] ) –

    List of validation errors, or empty list if validation succeeded.

Raises:

Source code in src/pupil_labs/realtime_api/models.py
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
def validate_answers(
    self,
    answers: dict[str, list[str]],
    template_format: TemplateDataFormat,
    raise_exception: bool = True,
) -> list[ErrorDetails]:
    """Validate answers for this Template.

    Args:
        answers: Answers to validate.
        raise_exception: Whether to raise an exception on validation failure.
        template_format: Format of the answers ("simple" or "api").

    Returns:
        list: List of validation errors, or empty list if validation succeeded.

    Raises:
        InvalidTemplateAnswersError: If validation fails and raise_exception is
        True.

    """
    AnswerModel = self._create_answer_model(template_format=template_format)
    errors = []
    try:
        AnswerModel(**answers)
    except ValidationError as e:
        errors = e.errors()

    for error in errors:
        question_id = error["loc"][0]
        question = self.get_question_by_id(str(question_id))
        if question:
            error["question"] = asdict(question)  # type: ignore[typeddict-unknown-key]

    if errors and raise_exception:
        raise InvalidTemplateAnswersError(self, answers, errors)
    return errors

Get Template Data

Using the device.get_template_data method, you can receive the responses currently saved in the template.

        data = await device.get_template_data(template_format="simple")

Set Template Data

Using the device.post_template_data method, you can set the template responses remotely.

            await device.post_template_data(questionnaire)

Get Template Questions & Validate them

You can also retrieve individual questions by their ID using the template.get_question_by_id method and check the validity of a response using the template.validate_answers method.

See it in action

Check the whole example code here
templates.py
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
import asyncio
import os

import beaupy

from pupil_labs.realtime_api import Device, Network
from pupil_labs.realtime_api.models import InvalidTemplateAnswersError, TemplateItem

LINE = "\u2500" * os.get_terminal_size().columns
RED = "\033[31m"
RESET = "\033[0m"


def prompt_checkbox_answer(item: TemplateItem, current_value):
    ticked = []
    for i, choice in enumerate(item.choices):
        current_value: list
        if choice in (current_value or []):
            current_value.remove(choice)
            ticked.append(i)
    choices = beaupy.select_multiple(
        item.choices,
        ticked_indices=ticked,
    )
    return choices


def prompt_radio_answer(item: TemplateItem, current_value):
    cursor_index = 0
    if current_value and current_value in item.choices:
        cursor_index = item.choices.index(current_value[0])

    choice = beaupy.select(item.choices, cursor_index=cursor_index)
    template_input = []
    if choice is not None:
        template_input = [choice]
    return template_input


def prompt_string_answer(item: TemplateItem, current_value):
    placeholder = item.help_text if item.help_text and item.help_text != [""] else None
    current_value = (
        placeholder if not current_value or current_value == [""] else current_value
    )
    return beaupy.prompt(
        f"Enter value for '{item.title}': ",
        initial_value="" if current_value is None else str(current_value),
    )


async def main():  # noqa: C901
    async with Network() as network:
        dev_info = await network.wait_for_new_device(timeout_seconds=5)
    if dev_info is None:
        print("No device could be found! Abort")
        return

    async with Device.from_discovered_device(dev_info) as device:
        # Fetch current template definition
        template = await device.get_template()
        # Fetch data filled on the template
        data = await device.get_template_data(template_format="simple")

        print(f"[{template.name}] Data pre-filled:")
        print(LINE)
        print("\n".join(f"{k}\t{v}" for k, v in data.items()))

        # Filling a template
        questionnaire = {}
        if template:
            try:
                for item in template.items:
                    if item.widget_type in ("SECTION_HEADER", "PAGE_BREAK"):
                        continue
                    print(LINE)
                    print(
                        f"{'* ' if item.required else ''}"
                        f"ID: {item.id} - Title: {item.title} "
                        f"- Input Type: {item.input_type}"
                    )
                    current_value = data.get(str(item.id))
                    while True:
                        question = template.get_question_by_id(item.id)
                        if item.widget_type == "CHECKBOX_LIST":
                            template_input = prompt_checkbox_answer(item, current_value)
                        elif item.widget_type == "RADIO_LIST":
                            template_input = prompt_radio_answer(item, current_value)
                        else:
                            template_input = prompt_string_answer(item, current_value)

                        try:
                            print(template_input)
                            errors = question.validate_answer(template_input)
                            if not errors:
                                questionnaire[str(item.id)] = template_input
                                break
                            else:
                                print(f"Errors: {errors}")
                        except InvalidTemplateAnswersError as e:
                            print(f"{RED}Validation failed for: {template_input}")
                            for error in e.errors:
                                print(f"    {error['msg']}")
                            print(LINE + RESET)
            except KeyboardInterrupt:
                print("\nKeyboardInterrupt detected. Skipping line.")

        print(LINE)

        # Sending the template
        if questionnaire:
            await device.post_template_data(questionnaire)

        # Fetch new data filled on the template
        data = await device.get_template_data(template_format="api")

        # Iterate to check filled data
        print(f"[{template.name}] Data post:")
        print(LINE)
        print("\n".join(f"{k}\t{v}" for k, v in data.items()))


if __name__ == "__main__":
    asyncio.run(main())