import logging
from typing import List, Optional
from pydantic import BaseModel, Field, model_validator, field_validator, ValidationError
[docs]
class LookerReference(BaseModel):
"""
This model represents the input you can set in alternative text for a shape in PowerPoint.
You can specify the different parameters to control how Looker data is fetched and displayed.
"""
id: str = Field(
...,
description="The ID of the Look or meta-look (meta_name) you want to reference.",
)
id_type: str = Field(
default="look",
description="The type of ID provided: 'look' or 'meta'. Defaults to 'look'."
" Setting to 'meta' indicates that the ID refers to a meta Look.",
)
meta: bool = Field(
default=False,
description="Set this to true if the Look is a meta Look. A meta look is a look that you want to retrieve and reuse, but not display directly.",
)
meta_name: str = Field(
default=None,
description="NOT actually working yet. If you are defining a meta look, you should provide a reference name here. This can then be used by other shapes to reference this meta look.",
)
meta_iterate: bool = Field(
default=False,
description="If set to true, this meta look will be iterated over by other shapes referencing it. This is useful for creating dynamic content based on the results of the meta look.",
)
label: str = Field(
default=None,
description="Setting a label here filters the results to the specified label. The label needs to match the specific column label from the look including any special characters.",
)
column: int = Field(
default=None,
description="The specific column to retrieve from the Look results. 0-indexed.",
)
row: int = Field(
default=None,
description="If you want to retrieve a specific row from the Look results, set the row number here (0-indexed).",
)
filter: str = Field(
default=None,
description="Define a lookml.field_name used in the Look that you want to be able to filter on using the --filter cli argument. Inputting --filter <value> will filter the results to where <label>=<value>.",
)
filter_overwrites: dict = Field(
default=None,
description="A dictionary of filter overwrites to apply to the Look. The keys are the filter lookml.field_names, and the values are the filter values. The filter values should not be enclosed in quotation marks. (unvalidated)",
)
result_format: str = Field(
default="json_bi",
description="The format to return the results in. Defaults to 'json_bi'.",
)
show_latest_chart_label: bool = Field(
default=False,
description="If set to true, modify chart series with labels to only show the latest label.",
)
apply_formatting: bool = Field(
default=False, description="Apply Looker-specified formatting to each result."
)
apply_vis: bool = Field(
default=True, description="Apply Looker visualization options to results."
)
server_table_calcs: bool = Field(
default=True,
description="Whether to compute table calculations on the Looker server before returning results.",
)
headers: bool = Field(
default=True,
description="Whether to overwrite headers in the result set with Looker-defined column labels.",
)
image_width: int = Field(
default=None,
description="Width of the image in pixels. Used for setting image size when asking looker to return a look rendered as an image.",
)
image_height: int = Field(
default=None,
description="Height of the image in pixels. Used for setting image size when asking looker to return a look rendered as an image.",
)
retries: int = Field(
default=0,
description="Number of retries for the Looker API request in case of failure. Defaults to 0.",
)
# optional parameters for the Look (Default to None)
[docs]
@field_validator("id", mode="before")
@classmethod
def convert_int(cls, value):
"""Validation: Convert integer values to strings."""
if isinstance(value, int):
return str(value)
return value
class LookerShape(BaseModel):
"""A Pydantic model for a shape in a PowerPoint presentation.
This model is used to define the properties of a shape, including its ID, type, dimensions,
and associated Looker reference.
"""
is_meta: bool = Field(
default=False, description="Whether this shape is a meta shape."
)
meta_name: str = Field(
default=None, description="The name of the meta shape, if applicable."
)
shape_id: str
shape_type: str
slide_number: int
shape_width: int = Field(default=None) # Width in pixels
shape_height: int = Field(default=None) # Height in pixels
integration: LookerReference
original_integration: LookerReference = Field(
default=None,
description="The original integration data before any modifications.",
)
shape_number: int = Field(
default=None, description="The number of the shape in the slide."
)
@model_validator(mode="before")
@classmethod
def push_down_relevant_data(cls, data):
"""Push down relevant data from the integration to the shape model."""
# push down
# if picture is shape type, then we need to push down the image width and height
if type(data.get("integration")) in (dict, LookerReference):
data["original_integration"] = data["integration"]
if data["shape_type"] == "PICTURE":
data["integration"]["result_format"] = data["integration"].get(
"result_format", "json_bi"
)
data["integration"]["image_width"] = round(data["shape_width"])
data["integration"]["image_height"] = round(data["shape_height"])
elif data["shape_type"] == "TABLE":
if data["integration"].get("apply_formatting") is None:
data["integration"]["apply_formatting"] = True
return data
class GeminiConfig(BaseModel):
"""
Configuration for a Gemini LLM text synthesis shape.
Set ``type: gemini`` in the alt text of a **text box** shape to enable this feature.
The Gemini model receives an assembled context built from the ordered
``contexts`` list, then produces replacement text for the shape.
Each entry in ``contexts`` is resolved by type:
* ``"self"`` — the shape's own current text (before synthesis).
* ``"slide_self"`` — text of all other shapes on the same slide after Looker
data has been rendered (i.e. the slide this comment will appear on).
* Any string starting with ``gemini_`` — the synthesized output of another
Gemini text box whose ``gemini_id`` matches. Those boxes are automatically
processed first.
* Anything else — treated as the ``meta_name`` of a Looker meta-look shape;
its pre-fetched data is formatted as a readable table.
.. note::
Requires the ``google-genai`` package. Install it with::
pip install looker_powerpoint[llm]
The ``GOOGLE_API_KEY`` (or ``GEMINI_API_KEY``) environment variable must also
be set.
"""
type: str = Field(
default="gemini",
description="Must be 'gemini' to identify this as a Gemini synthesis config.",
)
gemini_id: Optional[str] = Field(
default=None,
description=(
"A unique identifier for this Gemini shape within the presentation. "
"The ``gemini_`` prefix is added automatically if omitted. "
"Required if another Gemini shape references this box via its contexts list."
),
)
prompt: Optional[str] = Field(
default=None,
description="An optional instruction/question sent to the Gemini model together with the context data.",
)
contexts: List[str] = Field(
default_factory=list,
description=(
"Ordered list of context references for this Gemini shape. Each entry "
"is one of: ``'self'``, ``'slide_self'``, a ``gemini_<id>`` string "
"referencing another Gemini box, or a Looker meta-look ``meta_name``."
),
)
model: str = Field(
default="gemini-2.0-flash",
description="The Gemini model name to use for synthesis.",
)
@field_validator("type")
@classmethod
def type_must_be_gemini(cls, v):
if v != "gemini":
raise ValueError("type must be 'gemini' for GeminiConfig")
return v
@field_validator("gemini_id", mode="before")
@classmethod
def ensure_gemini_prefix(cls, v):
"""Auto-add the ``gemini_`` prefix when the user omits it."""
if v is not None and not str(v).startswith("gemini_"):
return f"gemini_{v}"
return v
class GeminiShape(BaseModel):
"""
A Pydantic model for a PowerPoint text-box shape configured for Gemini LLM synthesis.
"""
shape_id: str
shape_type: str
slide_number: int
shape_width: Optional[int] = Field(default=None)
shape_height: Optional[int] = Field(default=None)
integration: GeminiConfig
shape_number: Optional[int] = Field(default=None)