Source code for gformlib.builder

"""Google Forms API request builder.

This module converts a validated :class:`~gformlib.models.FormConfig` into
the JSON structures expected by the
`Google Forms REST API v1 <https://developers.google.com/forms/api/reference/rest>`_.

The main entry-point is :class:`FormBuilder`.
"""

from __future__ import annotations

from typing import Any, Dict, List

from .models import FormConfig, QuestionConfig, QuestionType, UpdateFormConfig


[docs] class FormBuilder: """Converts a :class:`~gformlib.models.FormConfig` into Google Forms API payloads. This class is used internally by :class:`~gformlib.client.GoogleFormsClient` but is also exposed publicly so that callers can inspect or further customise the generated request bodies before submitting them. Example:: from gformlib.builder import FormBuilder from gformlib.models import FormConfig, QuestionConfig, QuestionType config = FormConfig( title="Demo", questions=[ QuestionConfig( title="Favourite colour?", question_type=QuestionType.SHORT_ANSWER, ), ], ) builder = FormBuilder(config) create_body = builder.build_create_body() batch_body = builder.build_batch_update_body() """ def __init__(self, config: FormConfig) -> None: """Initialise the builder with a validated form configuration. Args: config: A :class:`~gformlib.models.FormConfig` instance. Use :func:`~gformlib.utils.parse_form_config` to obtain one from a raw dict. """ self._config = config # ────────────────────────────────────────────────────────────────────── # Public helpers # ──────────────────────────────────────────────────────────────────────
[docs] def build_create_body(self) -> Dict[str, Any]: """Build the request body for the ``forms().create()`` API call. Returns: A dict suitable for passing as the ``body`` argument of ``service.forms().create(body=...)``. Contains only the form title and document title; questions are added separately via :meth:`build_batch_update_body`. Example:: body = builder.build_create_body() # {"info": {"title": "Demo", "documentTitle": "Demo"}} """ return { "info": { "title": self._config.title, "documentTitle": self._config.document_title or self._config.title, } }
[docs] def build_batch_update_body(self) -> Dict[str, Any]: """Build the ``batchUpdate`` request body that adds all questions. Returns: A dict suitable for passing as the ``body`` argument of ``service.forms().batchUpdate(formId=..., body=...)``. Returns an empty ``{"requests": []}`` dict when the form has no questions. Example:: body = builder.build_batch_update_body() # {"requests": [{"createItem": {...}}, ...]} """ requests: List[Dict[str, Any]] = [] # Optionally set the form description via an updateFormInfo request if self._config.description: requests.append(self._build_update_form_info_request()) for index, question in enumerate(self._config.questions): requests.append(self._build_create_item_request(question, index)) return {"requests": requests}
[docs] @classmethod def build_update_body( cls, update_config: UpdateFormConfig, start_index: int = 0, ) -> Dict[str, Any]: """Build a ``batchUpdate`` request body for updating an existing form. Constructs a list of API requests based on what is set in *update_config*: * If :attr:`~gformlib.models.UpdateFormConfig.title` is set, an ``updateFormInfo`` request with mask ``"title"`` is included. * If :attr:`~gformlib.models.UpdateFormConfig.description` is set, an ``updateFormInfo`` request with mask ``"description"`` is included (combined with the title mask when both are provided). * Each question in :attr:`~gformlib.models.UpdateFormConfig.add_questions` generates a ``createItem`` request positioned after the existing items (controlled by *start_index*). Args: update_config: The validated update configuration. start_index: Zero-based index of the first new item. Pass the current number of items in the form so that new questions are appended at the end. Defaults to ``0``. Returns: A dict suitable for ``service.forms().batchUpdate(body=...)``. Returns ``{"requests": []}`` when *update_config* contains no changes. Example:: body = FormBuilder.build_update_body( UpdateFormConfig(title="New Title"), ) # {"requests": [{"updateFormInfo": {"info": {"title": "New Title"}, # "updateMask": "title"}}]} """ requests: List[Dict[str, Any]] = [] # --- Info update (title and/or description) ----------------------- info: Dict[str, Any] = {} masks: List[str] = [] if update_config.title is not None: info["title"] = update_config.title masks.append("title") if update_config.description is not None: info["description"] = update_config.description masks.append("description") if masks: requests.append( { "updateFormInfo": { "info": info, "updateMask": ",".join(masks), } } ) # --- Append new questions ----------------------------------------- # Use a temporary builder instance to reuse the existing question # serialisation logic without duplicating it. if update_config.add_questions: _builder = cls(FormConfig(title="", questions=update_config.add_questions)) for offset, question in enumerate(update_config.add_questions): requests.append( _builder._build_create_item_request(question, start_index + offset) ) return {"requests": requests}
# ────────────────────────────────────────────────────────────────────── # Private builders # ────────────────────────────────────────────────────────────────────── def _build_update_form_info_request(self) -> Dict[str, Any]: """Return a ``updateFormInfo`` mutation for the form description. Returns: A single ``updateFormInfo`` request dict. """ return { "updateFormInfo": { "info": {"description": self._config.description}, "updateMask": "description", } } def _build_create_item_request(self, question: QuestionConfig, index: int) -> Dict[str, Any]: """Return a ``createItem`` request for a single question. Args: question: The question configuration to convert. index: Zero-based insertion index (determines question order). Returns: A ``createItem`` request dict. """ question_body = self._build_question_body(question) item: Dict[str, Any] = { "title": question.title, "questionItem": {"question": question_body}, } if question.description: item["description"] = question.description return { "createItem": { "item": item, "location": {"index": index}, } } def _build_question_body(self, question: QuestionConfig) -> Dict[str, Any]: """Build the ``question`` sub-object inside a ``questionItem``. Args: question: Source question configuration. Returns: A ``question`` dict containing ``required`` and the type-specific sub-key (e.g. ``textQuestion``, ``choiceQuestion``). """ body: Dict[str, Any] = {"required": question.required} qtype = question.question_type if qtype in (QuestionType.SHORT_ANSWER, QuestionType.PARAGRAPH): body["textQuestion"] = {"paragraph": qtype == QuestionType.PARAGRAPH} elif qtype in ( QuestionType.MULTIPLE_CHOICE, QuestionType.CHECKBOXES, QuestionType.DROPDOWN, ): body["choiceQuestion"] = self._build_choice_question(question) elif qtype == QuestionType.SCALE: body["scaleQuestion"] = self._build_scale_question(question) elif qtype == QuestionType.DATE: body["dateQuestion"] = { "includeTime": question.include_time, "includeYear": question.include_year, } elif qtype == QuestionType.TIME: body["timeQuestion"] = {"duration": question.is_duration} elif qtype == QuestionType.FILE_UPLOAD: # FILE_UPLOAD requires the form to be in quiz mode and connected # to Drive; we emit the minimal required payload here. body["fileUploadQuestion"] = {} return body @staticmethod def _build_choice_question(question: QuestionConfig) -> Dict[str, Any]: """Build a ``choiceQuestion`` sub-object. Args: question: Source question configuration. ``question.options`` must be a non-empty list. Returns: A ``choiceQuestion`` dict. """ type_map = { QuestionType.MULTIPLE_CHOICE: "RADIO", QuestionType.CHECKBOXES: "CHECKBOX", QuestionType.DROPDOWN: "DROP_DOWN", } return { "type": type_map[question.question_type], "options": [{"value": opt} for opt in (question.options or [])], "shuffle": question.shuffle_options, } @staticmethod def _build_scale_question(question: QuestionConfig) -> Dict[str, Any]: """Build a ``scaleQuestion`` sub-object. Args: question: Source question configuration. Returns: A ``scaleQuestion`` dict. """ payload: Dict[str, Any] = { "low": question.low, "high": question.high, } if question.low_label: payload["lowLabel"] = question.low_label if question.high_label: payload["highLabel"] = question.high_label return payload