Skip to content

layout

layout

Page layout and content management subentity.

DomoPageLayout dataclass

DomoPageLayout(
    parent: DomoEntity,
    layout: PageLayout | None = None,
    cards: list[Any] = list(),
    datasets: list[Any] = list(),
)

Bases: DomoSubEntity

Page layout and content management subentity.

Manages page layout configuration and content operations including cards and datasets. Combines layout management with content operations that require page definition.

Attributes:

Name Type Description
layout PageLayout | None

PageLayout configuration object

cards list[Any]

list of DomoCard objects on the page

datasets list[Any]

list of DomoDataset objects used by page cards

Example

page = await DomoPage.get_by_id(page_id="123", auth=auth)

Get cards from page

await page.Layout.get_cards() print(f"Found {len(page.Layout.cards)} cards")

Update layout

await page.Layout.update(layout_changes={...})

get async

get(
    *, context: RouteContext | None = None, **context_kwargs
) -> PageLayout

Fetch the page layout from the v4/layouts API.

Retrieves the full layout including content slots and grid templates. Stores the result in self.layout for subsequent operations.

Parameters:

Name Type Description Default
context RouteContext | None

Optional RouteContext for API call configuration

None
**context_kwargs

Additional context parameters

{}

Returns:

Type Description
PageLayout

PageLayout object with content slots and grid templates

Source code in src/crew_dcs/classes/DomoPage/layout.py
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
async def get(
    self,
    *,
    context: RouteContext | None = None,
    **context_kwargs,
) -> PageLayout:
    """Fetch the page layout from the v4/layouts API.

    Retrieves the full layout including content slots and grid templates.
    Stores the result in self.layout for subsequent operations.

    Args:
        context: Optional RouteContext for API call configuration
        **context_kwargs: Additional context parameters

    Returns:
        PageLayout object with content slots and grid templates
    """
    context = RouteContext.build_context(context=context, **context_kwargs)

    # First, get the layout ID from the page definition
    if not self.layout:
        res = await page_routes.get_page_by_id(
            auth=self.parent.auth,
            page_id=self.parent.id,
            include_layout=True,
            context=context,
        )
        page_data = res.response
        if isinstance(page_data, dict):
            page_data = util_dd.DictDot(page_data)
        if hasattr(page_data, "pageLayoutV4") and page_data.pageLayoutV4:
            self.layout = PageLayout.from_dict(dd=page_data.pageLayoutV4)

    if not self.layout:
        # Fallback: try the direct layout endpoint if we know the layout_id
        raise ValueError(
            f"Could not determine layout ID for page {self.parent.id}. "
            "Fetch the page definition first with include_layout=True."
        )

    # Now fetch the full layout from the v4 endpoint (more complete than pageLayoutV4)
    res = await page_routes.get_page_layout(
        auth=self.parent.auth,
        layout_id=str(self.layout.id),
        context=context,
    )
    dd = (
        util_dd.DictDot(res.response)
        if isinstance(res.response, dict)
        else res.response
    )
    self.layout = PageLayout.from_dict(dd=dd)
    return self.layout

get_cards async

get_cards(
    return_raw: bool = False,
    *,
    context: RouteContext | None = None,
    parent_auth_retrieval_fn: (
        Callable[[str], Any] | None
    ) = None,
    check_if_published: bool | None = None,
    **context_kwargs
)

Get all cards from the page.

Fetches page definition and returns all cards, optionally checking if they're published.

Parameters:

Name Type Description Default
return_raw bool

Return raw ResponseGetData without processing

False
context RouteContext | None

Optional RouteContext for API call configuration

None
parent_auth_retrieval_fn Callable[[str], Any] | None

Callable returning publisher auth when given publisher domain

None
check_if_published bool | None

Check if cards are published (requires subscription check)

None
**context_kwargs

Additional context parameters

{}

Returns:

Type Description

list of DomoCard objects or ResponseGetData if return_raw=True

Source code in src/crew_dcs/classes/DomoPage/layout.py
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
async def get_cards(
    self,
    return_raw: bool = False,
    *,
    context: RouteContext | None = None,
    parent_auth_retrieval_fn: Callable[[str], Any] | None = None,
    check_if_published: bool | None = None,
    **context_kwargs,
):
    """Get all cards from the page.

    Fetches page definition and returns all cards, optionally checking if they're published.

    Args:
        return_raw: Return raw ResponseGetData without processing
        context: Optional RouteContext for API call configuration
        parent_auth_retrieval_fn: Callable returning publisher auth when given publisher domain
        check_if_published: Check if cards are published (requires subscription check)
        **context_kwargs: Additional context parameters

    Returns:
        list of DomoCard objects or ResponseGetData if return_raw=True
    """
    from .. import DomoCard as dc

    context = RouteContext.build_context(context=context, **context_kwargs)

    res = await page_routes.get_page_definition(
        auth=self.parent.auth, page_id=self.parent.id, context=context
    )

    if return_raw:
        return res

    if len(res.response.get("cards")) == 0:
        return []

    check_publish = True if check_if_published is None else check_if_published

    self.cards = await dmce.gather_with_concurrency(
        n=60,
        *[  # noqa: B026
            dc.DomoCard.get_by_id(
                card_id=card["id"],
                auth=self.parent.auth,
                parent_auth_retrieval_fn=parent_auth_retrieval_fn,
                check_if_published=check_publish,
                context=context,
            )
            for card in res.response.get("cards")
        ],
    )

    # Auto-fetch datasets with lineage for each card when parent_auth_retrieval_fn is provided
    if parent_auth_retrieval_fn:
        await dmce.gather_with_concurrency(
            n=60,
            *[  # noqa: B026
                card.Datasets.get(
                    parent_auth_retrieval_fn=parent_auth_retrieval_fn,
                    context=context,
                )
                for card in self.cards
                if card.Datasets
            ],
        )

    return self.cards

get_datasets async

get_datasets(
    return_raw: bool = False,
    *,
    context: RouteContext | None = None,
    **context_kwargs
)

Get all datasets used by cards on the page.

Parameters:

Name Type Description Default
return_raw bool

Return raw ResponseGetData without processing

False
context RouteContext | None

Optional RouteContext for API call configuration

None
**context_kwargs

Additional context parameters

{}

Returns:

Type Description

list of DomoDataset objects or ResponseGetData if return_raw=True

Source code in src/crew_dcs/classes/DomoPage/layout.py
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
async def get_datasets(
    self,
    return_raw: bool = False,
    *,
    context: RouteContext | None = None,
    **context_kwargs,
):
    """Get all datasets used by cards on the page.

    Args:
        return_raw: Return raw ResponseGetData without processing
        context: Optional RouteContext for API call configuration
        **context_kwargs: Additional context parameters

    Returns:
        list of DomoDataset objects or ResponseGetData if return_raw=True
    """
    context = RouteContext.build_context(context=context, **context_kwargs)

    res = await page_routes.get_page_definition(
        auth=self.parent.auth, page_id=self.parent.id, context=context
    )

    if return_raw:
        return res

    cards = await self.get_cards(context=context)

    card_datasets = await dmce.gather_with_concurrency(
        *[card.get_datasets(context=context) for card in cards],
        n=10,
    )

    self.datasets = [
        ds for ds_ls in card_datasets for ds in ds_ls if ds is not None
    ]

    return self.datasets

swap_cards async

swap_cards(
    card_mappings: dict[int, int],
    *,
    context: RouteContext | None = None,
    **context_kwargs
) -> bool

Swap card IDs in the layout content slots.

Replaces cards in the layout by content_key. Acquires write lock, updates layout, then releases lock.

Appendix behavior: The Domo layout API is append-only for content slots. When you change a cardId on a slot, the old card is NOT deleted — it gets displaced to the "appendix" (a hidden section of the page where unplaced content lives). Each swap adds a HEADER + the old card to the appendix. Repeated swaps will accumulate orphaned cards in the appendix. To clean up, use the Domo UI to delete appendix content, or call remove_appendix() after swapping.

Parameters:

Name Type Description Default
card_mappings dict[int, int]

Dict mapping content_key → new card_id. Example: {0: 123456, 2: 789012} replaces the card at content_key 0 with card 123456 and content_key 2 with card 789012.

required
context RouteContext | None

Optional RouteContext for API call configuration

None
**context_kwargs

Additional context parameters

{}

Returns:

Type Description
bool

True if successful, False otherwise

Source code in src/crew_dcs/classes/DomoPage/layout.py
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
async def swap_cards(
    self,
    card_mappings: dict[int, int],
    *,
    context: RouteContext | None = None,
    **context_kwargs,
) -> bool:
    """Swap card IDs in the layout content slots.

    Replaces cards in the layout by content_key. Acquires write lock,
    updates layout, then releases lock.

    **Appendix behavior**: The Domo layout API is append-only for content
    slots. When you change a cardId on a slot, the old card is NOT deleted —
    it gets displaced to the "appendix" (a hidden section of the page where
    unplaced content lives). Each swap adds a HEADER + the old card to the
    appendix. Repeated swaps will accumulate orphaned cards in the appendix.
    To clean up, use the Domo UI to delete appendix content, or call
    remove_appendix() after swapping.

    Args:
        card_mappings: Dict mapping content_key → new card_id.
            Example: {0: 123456, 2: 789012} replaces the card at
            content_key 0 with card 123456 and content_key 2 with card 789012.
        context: Optional RouteContext for API call configuration
        **context_kwargs: Additional context parameters

    Returns:
        True if successful, False otherwise
    """
    context = RouteContext.build_context(context=context, **context_kwargs)

    if not self.layout:
        await self.get(context=context)

    # Apply card swaps to content slots
    for content_item in self.layout.content:
        if content_item.content_key in card_mappings:
            new_card_id = card_mappings[content_item.content_key]
            content_item.card_id = new_card_id
            content_item.card_urn = str(new_card_id)

    # Push the updated layout (update() uses self.layout.get_body() internally)
    return await self.update(context=context)

update async

update(
    body: dict | None = None,
    *,
    context: RouteContext | None = None,
    **context_kwargs
)

Update page layout.

Acquires write lock, updates layout, then releases lock. If no body is provided, uses self.layout.get_body().

Parameters:

Name Type Description Default
body dict | None

Layout configuration dictionary (optional — uses self.layout.get_body() if not provided)

None
context RouteContext | None

Optional RouteContext for API call configuration

None
**context_kwargs

Additional context parameters

{}

Returns:

Type Description

True if successful, False otherwise

Source code in src/crew_dcs/classes/DomoPage/layout.py
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
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
774
775
776
async def update(
    self,
    body: dict | None = None,
    *,
    context: RouteContext | None = None,
    **context_kwargs,
):
    """Update page layout.

    Acquires write lock, updates layout, then releases lock.
    If no body is provided, uses self.layout.get_body().

    Args:
        body: Layout configuration dictionary (optional — uses self.layout.get_body() if not provided)
        context: Optional RouteContext for API call configuration
        **context_kwargs: Additional context parameters

    Returns:
        True if successful, False otherwise
    """
    context = RouteContext.build_context(context=context, **context_kwargs)

    if not self.layout:
        # Need layout ID - fetch it first
        res = await page_routes.get_page_by_id(
            auth=self.parent.auth,
            page_id=self.parent.id,
            include_layout=True,
            context=context,
        )
        page_data = res.response
        if isinstance(page_data, dict):
            page_data = util_dd.DictDot(page_data)
        if hasattr(page_data, "pageLayoutV4") and page_data.pageLayoutV4:
            self.layout = PageLayout.from_dict(dd=page_data.pageLayoutV4)

    if not self.layout:
        return False

    layout_id = self.layout.id
    layout_body = body or self.layout.get_body()

    datetime_now = dt.datetime.now()
    start_time_epoch = dmcv.convert_datetime_to_epoch_millisecond(datetime_now)

    res_writelock = await page_routes.put_writelock(
        auth=self.parent.auth,
        layout_id=layout_id,
        user_id=self.parent.auth.user_id,
        epoch_time=start_time_epoch,
        context=context,
    )

    if res_writelock.status == 200:
        res = await page_routes.update_page_layout(
            auth=self.parent.auth,
            body=layout_body,
            layout_id=layout_id,
            context=context,
        )

        if not res.is_success:
            return False

        res_writelock = await page_routes.delete_writelock(
            auth=self.parent.auth, layout_id=layout_id, context=context
        )
        if res_writelock.status != 200:
            return False

    else:
        return False

    return True

PageLayoutContent dataclass

PageLayoutContent(
    content_key: int,
    type: str,
    id: int | None,
    card_id: int | None,
    card_urn: str | None,
    text: str | None,
    accept_date_filter: bool | None,
    accept_filters: bool | None,
    accept_segments: bool | None,
    compact_interaction_default: bool,
    fit_to_frame: bool | None,
    has_summary: bool | None,
    hide_border: bool | None,
    hide_description: bool | None,
    hide_footer: bool | None,
    hide_margins: bool | None,
    hide_summary: bool | None,
    hide_timeframe: bool | None,
    hide_title: bool | None,
    hide_wrench: bool | None,
    summary_number_only: bool | None,
    edit_in_app_viewer: bool = True,
    show_more_content: bool = False,
    chart_styles: bool | None = None,
    background_id: int | None = None,
    background: PageLayoutBackground | None = None,
    tab_type: str | None = None,
    collapsible: bool | None = None,
    style: dict | None = None,
    icon: dict | None = None,
    icon_position: str | None = None,
    variable_control_id: int | None = None,
    variable_control_urn: str | None = None,
)

A content slot in a page layout.

Content types and their key fields: - CARD: has cardId/cardUrn, all display flags are bool, optional style dict - BUTTON: has text, most display flags are null - TABS: container for tab navigation, has tab_type/collapsible/style/hide_title - TAB_CONTENT: has text (tab label), icon, icon_position - VARIABLE: filter/variable control, display flags are bool - FORM: form input, most display flags are null - HEADER: has text (section label), display flags are bool, id can be null

Type-specific fields (null for other types): - tab_type: TABS only — e.g. "TAB" - collapsible: TABS only — whether tabs can collapse - style: TABS and CARD — visual styling dict with sourceId, textColor, etc. - icon: TAB_CONTENT only — dict with value (icon name) and size - icon_position: TAB_CONTENT only — e.g. "LEFT"